Bug with equals operator and NSObjects in Swift 2.0?

1.2k views Asked by At

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?

3

There are 3 answers

3
Qbyte On BEST ANSWER

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.

0
david-hoze On

kylealanhale's answer does not work with NSManagedObject (explained here), so I created a new protocol NSObjectSubclassEquatable that you can use for comparing NSobject subclasses.

infix operator =~= {}

public protocol NSObjectSubclassEquatable {

  static func compare(lhs: Self,_ rhs: Self) -> Bool
}


public func =~=<T : NSObjectSubclassEquatable>(lhs: T, rhs: T) -> Bool {

  return T.compare(lhs, rhs)
}

func =~=<Element : NSObjectSubclassEquatable>(lhs: [Element], rhs: [Element]) -> Bool {
  for (lhsElement,rhsElement) in zip(lhs, rhs) {
    if !(lhsElement =~= rhsElement) {
      return false
    }
  }
  return true
}

Example:

class Point: NSObject {

  var x: Int
  var y: Int

  init(_ x: Int,_ y: Int) {
    self.x = x
    self.y = y
  }
}

extension Point: NSObjectSubclassEquatable {

  static func compare(lhs: Point,_ rhs: Point) -> Bool {
    return lhs.x == rhs.x && lhs.y == rhs.y
  }
}

Point(1,2) =~= Point(1,2) // Prints true
[Point(1,2)] =~= [Point(1,2)] // Prints true
0
kylealanhale On

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/