November 12, 2016
Update (4/27/2020): 4 years after writing this article, I realized that it could be improved on a little bit. Added info about the recommended way to override equality for NSObjects, and a note about how Swiftlint has a rule to handle this.
Swift can be tricky sometimes. For example, what does the following print?
class A: NSObject {
let x: Int
init(x: Int) {
self.x = x
}
}
func ==(left: A, right: A) -> Bool {
return left.x == right.x
}
print(A(x: 1) == A(x: 1))
print([A(x: 1)] == [A(x: 1)])
Perhaps surprisingly, it's this:
true
false // huh?!
Why does comparing two arrays with equal contents return false? It turns out that:
==(_:_:) function, so it falls back to NSObject's ==(_:_:), which calls isEqualisEqual compares objects by casting to AnyHashableWe can see that this is the problem by casting the objects to AnyHashable before comparing them:
print(A(x: 1) as AnyHashable == A(x: 1) as AnyHashable)
false
Apparently, doing this will use the NSObject implementation of ==(_:_:) rather than the A one. The NSObject implementation of ==(_:_:) checks isEqual, which returns false because the two elements aren't the same in memory. We can see this by adding an override:
class B: NSObject {
let x: Int
init(x: Int) {
self.x = x
}
override func isEqual(_ object: Any?) -> Bool {
print("isEqual for B")
return super.isEqual(object)
}
}
func ==(left: B, right: B) -> Bool {
print("== for B")
return left.x == right.x
}
Then,
print([B(x: 1)] == [B(x: 1)])
isEqual for B
false
The best way to make an NSObject subclass use custom equality inside an array is to override isEqual:
class C: NSObject {
let x: Int
init(x: Int) {
self.x = x
}
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? C else { return false }
return object.x == self.x
}
}
print([C(x: 1)] == [C(x: 1)])
true
Of course, if we defined A as a struct or a regular class in the first place, there won't be ==(_:_:) defined for [A], which means the compiler would catch our mistake instead of falling back to something that might not work correctly.
If you use Swiftlint, there's actually a rule you can use to prefer the recommended way of overriding isEqual: nsobject_prefer_isequal. Highly recommended!
A working playground with the code from this post can be found here.
I'm Noah, a software developer based in the San Francisco Bay Area. I focus mainly on full stack web and iOS development