Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bug with equals operator and NSObjects in Swift 2.0?

Ok, something strange is happening when writing your own equals operator for NSObject subclasses in Swift 2.0 like this:

func ==(lhs: MyObject, rhs: MyObject) -> Bool {
    return lhs.identifier == rhs.identifier
}

For a class that looks like this:

class MyObject: NSObject {
    let identifier: String
    init(identifier: String) {
        self.identifier = identifier
    }
}

This used to work just fine in Swift 1.2 and below. It still kind of works:

let myObject1 = MyObject(identifier: "A")
let myObject2 = MyObject(identifier: "A")
let result = (myObject1 == myObject2)
// result is true

So far so good, but what if both of the variables were optionals?

let myObject1: MyObject? = MyObject(identifier: "A")
let myObject2: MyObject? = MyObject(identifier: "A")
let result = (myObject1 == myObject2)
// result is false, equals operator was never even called

And one other thing that no longer works:

let myObject1 = MyObject(identifier: "A")
let myObject2 = MyObject(identifier: "A")
let result = (myObject1 == myObject2)
// result is true
let result = (myObject1 != myObject2)
// result is true, equals operator was never even called

So apparently, != no longer calls the == operator and negates it. It seems to just compare the instances instead when using !=

All of this only happens when your class is a subclass of NSObject (directly or indirectly). When it's not, everything works just like you would expect.

Can anyone tell me if this is a new 'feature' in Swift 2.0 or just a nasty bug?

like image 315
Tom van Zummeren Avatar asked Jun 28 '15 11:06

Tom van Zummeren


2 Answers

I think this behavior should be considered a bug (still present as of Xcode 7 beta 6), but there's a hopefully temporary workaround: override NSObject's -isEqual instead of implementing Swift's == operator.

class MyObject: NSObject {
    let identifier: String
    init(identifier: String) {
        self.identifier = identifier
    }
    override func isEqual(object: AnyObject?) -> Bool {
        guard let rhs = object as? MyObject else {
            return false
        }
        let lhs = self

        return lhs.identifier == rhs.identifier
    }
}

I found another reference to the problem, with more code examples, here: http://mgrebenets.github.io/swift/2015/06/21/equatable-nsobject-with-swift-2/

like image 74
kylealanhale Avatar answered Oct 21 '22 12:10

kylealanhale


Unfortunately I don't know whether this is considered a feature or not (I don't think so). This problem occurs if any class subclasses a class which conforms to Equatable(like NSObject; it compares actual instances). So if you only "override" the == operator of the subclass all other operators like:

func !=<T : Equatable>(lhs: T, rhs: T) -> Bool
func ==<T : Equatable>(lhs: T?, rhs: T?) -> Bool
func ==<T : Equatable>(lhs: [T], rhs: [T]) -> Bool

where T is constrained to be Equatable Swift uses the == operator of the baseclass. As (time-consuming) workaround you can overload all the equality operators you have to use like so:

func !=(lhs: MyObject, rhs: MyObject) -> Bool { ... }
func ==(lhs: MyObject?, rhs: MyObject?) -> Bool { ... }
func ==(lhs: [MyObject], rhs: [MyObject]) -> Bool { ... }

Edit: The Reason

The reason for this behavior is that if a subclass conforms to Equatable the Self of the self requirement is determined to be this class. So every time the == is called with a (generic) type that conforms to Equatable it only calls the operator of the initial conforming class.

like image 41
Qbyte Avatar answered Oct 21 '22 11:10

Qbyte