1

We have multiple instances of a custom Swift class, which inherits from SKSpriteNode, and were able to execute the following code (grossly simplified for this question) correctly:

let instance1 = CustomClass()
let instance2 = CustomClass()
let instance3 = CustomClass()
let instance4 = CustomClass()

let array1 = [instance1, instance2]
let array2 = [instance3, instance4]

func check(testInstance: CustomClass) -> Bool {
   return array1.filter({ $0 == testInstance }).count > 0
}

check(testInstance: instance3)

In other words, executing check(testInstance: instance3) returned false as expected.

However, we made a bunch of changes, and check stopped working.

CustomClass does not implement the Equatable protocol. We just want to detect unique instances.

It only started working when we used ObjectIdentifier, meaning the function changed to this:

func check(testInstance: CustomClass) -> Bool {
   return array1.filter({ ObjectIdentifier($0) == ObjectIdentifier(testInstance) }).count > 0
}

Why is ObjectIdentifier needed, and when should it be used for object equality?

This was written with Swift 3.

Crashalot
  • 33,605
  • 61
  • 269
  • 439
  • It's basically `===`, see this: https://stackoverflow.com/questions/39587027/difference-between-using-objectidentifier-and-operator – Sweeper May 27 '17 at 10:15
  • @Hamish identity comparison, not looking for comparison of properties – Crashalot May 27 '17 at 21:24
  • @Sweeper why did it work before without ===, i.e., only using == worked? The goal is to understand that. – Crashalot May 27 '17 at 21:25
  • @Crashalot You just want `===` then. We can't know why it no longer works with `==` without seeing how the `==` overload for `CustomClass` is implemented (but semantically, `==` shouldn't be used for an identity comparison). Did you implement a `CustomClass` overload of `==`? If not, I suspect it inherits from `NSObject`, in which case it gets an `==` overload that just calls `isEquals(_:)` – so by default does an identity comparison. I guess you must have overriden `isEquals(_:)` in that case and implemented it to actually perform an equality check. – Hamish May 27 '17 at 21:31
  • @Hamish thanks for the fast reply. there was no `==` overload for `CustomClass`, this is what's baffling. – Crashalot May 27 '17 at 21:35
  • 1
    @Crashalot So it inherits from `NSObject`? If you command-click on the `==` operator, Xcode will take you to the overload that's being called. – Hamish May 27 '17 at 21:35
  • @Hamish great idea. it inherits from `SKNode`. – Crashalot May 27 '17 at 21:36
  • 1
    @Crashalot Which then inherits from `NSObject` :) So the implementation of `isEqual(_:)` is likely where you need to be looking. – Hamish May 27 '17 at 21:37
  • @Hamish sorry `SKSpriteNode` which is why `ios` was an original tag – Crashalot May 27 '17 at 21:37
  • 1
    @Hamish yup, just did command-click as you suggested, and it appears the `==` function from `NSObject` is used. – Crashalot May 27 '17 at 21:38
  • `array1.filter({ $0 == testInstance }).count > 0` -> just use `contains(where:)`: `array1.contains(where: { $0 == testInstance })` – Alexander Jan 18 '18 at 15:19

1 Answers1

3

Why is ObjectIdentifier needed, and when should it be used for object equality?

You don't need to use ObjectIdentifier in order to perform an identity comparison in this case, you can simply use the identity operator === instead which, as Martin says here, for class instances is equivalent to using ObjectIdentifier's == overload:

func check(testInstance: CustomClass) -> Bool {
    return array1.contains(where: { $0 === testInstance })
}

Also note we're using contains(where:) over filter{...}.count > 0, as the former will short-circuit upon finding a matching element, whereas the latter evaluates the entire sequence (and creates an unnecessary intermediate array).

The direct use of == to perform an identity comparison of objects may have worked due to the fact that CustomClass ultimately inherits from NSObject, which conforms to Equatable by defining an == overload that calls isEqual(_:), which by default performs an identity comparison.

However in general, this should not be relied upon – the implementation of isEqual(_:) can be overridden to perform a comparison based on property values rather than identity. Furthermore, semantically Equatable requires that the implementation of == is based all on visible aspects (i.e property values) of the instances being compared.

From the documentation:

Equality implies substitutability — any two instances that compare equally can be used interchangeably in any code that depends on their values. To maintain substitutability, the == operator should take into account all visible aspects of an Equatable type.

Therefore using == for an identity comparison on your class was never correct, even though it may have worked initially.

As for when ObjectIdentifier should be used, really you should never need it just to perform an identity comparison. For classes, you should use the === operator, and for metatypes, you should simply use the == overload defined specifically for them (as in that case, identity just happens to equality, as each new metatype instance is unique).

The main use of ObjectIdentifier is really its hashValue implementation, which is derived from the pointer value of the thing that it's initialised with. This can be useful, for example, in allowing metatypes to be Dictionary keys (compare Make a Swift dictionary where the key is "Type"?).

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • such a superb response, wish i could award you 10x the points :) – Crashalot May 28 '17 at 08:35
  • is `contains` new to swift 3? someone advised using `filter` before, but maybe that was before `contains` existed? thanks again! – Crashalot May 28 '17 at 08:36
  • @Crashalot Happy to help! And `contains` has been around since the days of Swift 1 :) Although back then it was a top-level function – it became an instance method of `Sequence(Type)` in Swift 2. – Hamish May 28 '17 at 09:25
  • Darn you and your 23K points worth of knowledge! You took away the only good excuse for using `filter` and not `contains`! Now all that's left is 'bad developer' hahaha. :) Thanks again! – Crashalot May 28 '17 at 19:17