Extreme implementation does not work with generics

I'm still fighting Swift generics. Today I found that my implementation of the Equatable protocol does not work if it is called from a 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? ..... } 

A class that uses a 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 this subclass:

 class TrackingCache<T: Tracking>: BaseCache<Tracking> { } 

When I call the removeEntities method of the TrackingCache instance, I always get equal: false in the output, even if the id matches.

But if I translate the method directly into the TrackingCache class, it looks fine!

Any ideas why this is happening and how to fix it?

+6
source share
1 answer

Beware: since == not a member function, it will not give you dynamic dispatch by default, including if you use it together with a common 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 

Now create two variables of type NSObject and assign them the same values:

 let o1: NSObject = c1 let o2: NSObject = c2 // this will be false o1 == o2 

Why? Because you call the function func ==(lhs: NSObject, rhs: NSObject) -> Bool , and not func ==(lhs: C, rhs: C) -> Bool . Which overloaded function for selection is not determined dynamically at runtime, depending on what o1 and o2 refer to. It is determined by Swift at compile time based on the types o1 and o2 , which in this case are NSObject .

NSObject == is implemented differently with your peers - it calls lhs.isEqual(rhs) , which is returned if not overridden to check for reference equality (i.e. are two references pointing to the same object). They arent therefore they are not equal.

Why does this happen with BaseCache , but not with TrackingCache ? Since BaseCache defined as a restriction only of NSObject , therefore T has only the capabilities of NSObject - just as you assigned c1 variable of type NSObject , the version of NSObject from == .

TrackingCache on the other hand guarantees T will be at least a Tracking object, therefore version == for tracking. Swift will choose the more "specific" of all the possible overloads - Tracking more specific than the base class, NSObject .

Here is a simpler example: just using common 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 method (having == call to the method of dynamically allocated classes) is also a way to implement this behavior for your classes other than NSObject. Structures, of course, do not have this problem, because they do not support inheritance - a rating of 1 for structures!

+12
source

Source: https://habr.com/ru/post/980635/


All Articles