NSObject Equality is Tricky

November 12, 2016

Swift can be tricky sometimes. For example, what does the following print?

class A: NSObject {
  let x: Int

  init(x: Int) {
    self.x = x
  }
}

func ==(left: A, right: A) -> Bool {
  return left.x == right.x
}

print(A(x: 1) == A(x: 1))
print([A(x: 1)] == [A(x: 1)])

Perhaps surprisingly, it's this:

true
false // huh?!

Equatable conformance is hard

Why does comparing two arrays with equal contents return false? It turns out that:

We can see that this is the problem by casting the objects to AnyHashable before comparing them:

print(A(x: 1) as AnyHashable == A(x: 1) as AnyHashable)
false

Apparently, doing this will use the NSObject implementation of ==(_:_:) rather than the A one. The NSObject implementation of ==(_:_:) checks isEqual, which returns false because the two elements aren't the same in memory. We can see this by adding an override:

class B: NSObject {
  let x: Int

  init(x: Int) {
    self.x = x
  }

  override func isEqual(_ object: Any?) -> Bool {
    print("isEqual for B")
    return super.isEqual(object)
  }
}

func ==(left: B, right: B) -> Bool {
  print("== for B")
  return left.x == right.x
}

Then,

print([B(x: 1)] == [B(x: 1)])
isEqual for B
false

The fix

As far as I can tell, the best way to make an NSObject subclass use custom equality inside an array is to override isEqual:

class C: NSObject {
  let x: Int

  init(x: Int) {
    self.x = x
  }

  override func isEqual(_ object: Any?) -> Bool {
    guard let object = object as? C else { return false }
    return object == self
  }
}

func ==(left: C, right: C) -> Bool {
  print("== for C")
  return left.x == right.x
}
print([C(x: 1)] == [C(x: 1)])
== for C
true

Of course, if we defined A as a struct or a regular class in the first place, there won't be ==(_:_:) defined for [A], which means the compiler would catch our mistake instead of falling back to something that might not work correctly.

Sigh. Working with NSObject can be tricky.

A working playground with the code from this post can be found here.