Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Equatable implementation doesn't seem working with generics

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 ids 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?

like image 562
Andrey Gordeev Avatar asked Jan 07 '15 08:01

Andrey Gordeev


1 Answers

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!

like image 81
Airspeed Velocity Avatar answered Sep 21 '22 11:09

Airspeed Velocity