[Pitch] Remove automatic Equatable conformance for NSObjects

The documentation for the Swift Equatable protocol states that it is:

A type that can be compared for value equality.

However, NSObjects implement the Equatable protocol by default using their isEqual method via an extension in Foundation. Per the docs from NSObject:

The default implementation for this method provided by NSObject returns true if an isEqualTo: message sent to the same object would return true.

This is not value equality. This is reference equality and we have a separate operator for this in Swift, ===. This is a frequent point of confusion for Swift developers working in Objective-C, as evidenced by a conversation we had today at Snap. Their expectation, as the documentation implied, was that this would perform a value equality test, not a reference equality test.

To this end, I propose removing the automatic conformance of NSObject to Equatable. This would obviously be a source-breaking change, though the auto-migrator could easily replace these with explicit isEqual calls. The only risk would be if someone had explicitly shadowed == in an NSObject subclass. This is quite uncommon, though. More common would be overriding isEqual in a subclass which would still be called.

Moving forwards, engineers would explicitly conform to the Equatable protocol when they actually want to do so, and would implement value equality as expected. If they want reference equality, they can use ===, NSObject's isEqual, or use == with the ObjectIdentifier's of two objects.

I'm a bit confused. The implementation of NSObject's isEqual method does a reference test (does other == self), but subclasses which add member variables are expected to override isEqual to compare the members. Swift's default implementation would then be doing the right thing by invoking isEqual.

It seems to me that your confusion was due to a misunderstanding on your part, and not due to a flaw in Swift's synthesized equality comparison for NSObject subclasses.

2 Likes

Hmm the docs that were included with my version of Xcode were a bit out of date. The online docs elaborate a bit further:

https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418795-isequal

This method defines what it means for instances to be equal. For example, a container object might define two containers as equal if their corresponding objects all respond YES to an isEqual: request. See the NSData , NSDictionary , NSArray , and NSString class specifications for examples of the use of this method.

If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual: in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.

This complicates matters a bit because Swift decouples the Hashable protocol from the Equatable protocol (though Hashable types must also conform to Equatable).

I'd still advocate for not giving Swift Equatable to all NSObjects, though.

If this is needed for bridging Swift objects into / out of Objective-C collections e.g. [String : AnyObject] <> NSDictionary there could be NSEquatable and NSHashable protocols defined in terms of isEqual and hash to which NSObjectProtocol could conform.

Hashable and Equatable could be extended to conform to their NS-equivalents, but NSObjects wouldn't implicitly get the Swift versions since these should provide more meaningful implementations of these methods.

"isEqual" (and NSObjects's "==" that uses it internally) works properly: it compares the "values"... just by default the "value" of NSObject is its reference, and if that's unwanted you can override "isEqual" (along with "hash") to give it the value semantics you are after.

I won't expect changes (especially source breaking) in Obj-C/swift interop. Moving forward if you haven't switched to swift yet (which is a long overdue) - it is never too late.

2 Likes