I am still fighting with Swift generics. Today I discovered that my implementation of Equatable protocol doesn't work, if it's called from generic class.
My model class:
func ==(lhs: Tracking, rhs: Tracking) -> Bool {
// This method never executes if called from BaseCache
return lhs.id == rhs.id
}
class Tracking: NSObject, Equatable, Printable {
var id: String?
.....
}
Class, which uses generic type:
class BaseCache<T: NSObject where T: Equatable, T: Printable> {
.....
func removeEntities(entities: [T]) {
var indexesToRemove = [Int]()
for i in 0...allEntities.count - 1 {
let item = allEntities[i]
for entity in entities {
println("equal: \(entity == item)")
// FOR SOME REASONS THE STATEMENT BELOW IS ALWAYS FALSE
if entity == item {
indexesToRemove.append(i)
break
}
}
}
for index in indexesToRemove {
allEntities.removeAtIndex(index)
}
didRemoveEntities()
}
}
and it's subclass:
class TrackingCache<T: Tracking>: BaseCache<Tracking> {
}
When I call removeEntities
method of TrackingCache
instance, I always get equal: false
in the output, even if id
s are the same.
But if I move the method to TrackingCache
class directly, it seems working fine!
Any ideas why this is happening and how to fix this?
Beware: since ==
is not a member function, it will not give you dynamic dispatch by default, including if you use it alongside a generic placeholder.
Consider the following code:
class C: NSObject, Equatable {
let id: Int
init(_ id: Int) { self.id = id }
}
// define equality as IDs are equal
func ==(lhs: C, rhs: C) -> Bool {
return lhs.id == rhs.id
}
// create two objects with the same ID
let c1 = C(1)
let c2 = C(1)
// true, as expected
c1 == c2
OK now create two variables of type NSObject
, and assign the same values to them:
let o1: NSObject = c1
let o2: NSObject = c2
// this will be false
o1 == o2
Why? Because you are invoking the function func ==(lhs: NSObject, rhs: NSObject) -> Bool
, not func ==(lhs: C, rhs: C) -> Bool
. Which overloaded function to choose is not determined dynamically at runtime based on what o1
and o2
refer to. It is determined by Swift at compile time, based on the types of o1
and o2
, which in this case is NSObject
.
NSObject
==
is implemented differently to your equals – it calls lhs.isEqual(rhs)
, which falls back if not overridden to checking reference equality (i.e. are the two references pointing at the same object). They aren’t, so they aren’t equal.
Why does this happen with BaseCache
but not with TrackingCache
? Because BaseCache
is defined as constraining only to NSObject
, so T
only has the capabilities of an NSObject
– similar to when you assigned c1
to a variable of type NSObject
, the NSObject
version of ==
will be called.
TrackingCache
on the other hand guarantees T
will be at least a Tracking
object, so the version of ==
for tracking is used. Swift will pick the more "specific" of all possible overloads – Tracking
is more specific than it's base class, NSObject
.
Here’s a simpler example, just with generic functions:
func f<T: NSObject>(lhs: T, rhs: T) -> Bool {
return lhs == rhs
}
func g<T: C>(lhs: T, rhs: T) -> Bool {
return lhs == rhs
}
f(c1, c2) // false
g(c1, c2) // true
If you want to fix this, you can override isEqual
:
class C: NSObject, Equatable {
...
override func isEqual(object: AnyObject?) -> Bool {
return (object as? C)?.id == id
}
}
// this is now true:
o1 == o2
// as is this:
f(c1, c2)
This technique (having ==
call a dynamically-dispatched class method) is also a way to implement this behaviour for your non-NSObject classes. Structs, of course, don’t have this issue since they don’t support inheritance – score 1 for structs!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With