ObjectIdentifier needed for Swift equality?

2019-02-28 07:02发布

问题:

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.

回答1:

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"?).