48
votes

When subclassing NSObject in Swift, should you override hash or implement Hashable? Also, should you override isEqual: or implement ==?

3

3 Answers

108
votes

NSObject already conforms to the Hashable protocol:

extension NSObject : Equatable, Hashable {
    /// The hash value.
    ///
    /// **Axiom:** `x == y` implies `x.hashValue == y.hashValue`
    ///
    /// - Note: the hash value is not guaranteed to be stable across
    ///   different invocations of the same program.  Do not persist the
    ///   hash value across program runs.
    public var hashValue: Int { get }
}

public func ==(lhs: NSObject, rhs: NSObject) -> Bool

I could not find an official reference, but it seems that hashValue calls the hash method from NSObjectProtocol, and == calls the isEqual: method (from the same protocol). See update at the end of the answer!

For NSObject subclasses, the correct way seems to be to override hash and isEqual:, and here is an experiment which demonstrates that:

1. Override hashValue and ==

class ClassA : NSObject {
    let value : Int
    
    init(value : Int) {
        self.value = value
        super.init()
    }
    
    override var hashValue : Int {
        return value
    }
}

func ==(lhs: ClassA, rhs: ClassA) -> Bool {
    return lhs.value == rhs.value
}

Now create two different instances of the class which are considered "equal" and put them into a set:

let a1 = ClassA(value: 13)
let a2 = ClassA(value: 13)

let nsSetA = NSSet(objects: a1, a2)
let swSetA = Set([a1, a2])

print(nsSetA.count) // 2
print(swSetA.count) // 2

As you can see, both NSSet and Set treat the objects as different. This is not the desired result. Arrays have unexpected results as well:

let nsArrayA = NSArray(object: a1)
let swArrayA = [a1]

print(nsArrayA.indexOfObject(a2)) // 9223372036854775807 == NSNotFound
print(swArrayA.indexOf(a2)) // nil

Setting breakpoints or adding debug output reveals that the overridden == operator is never called. I don't know if this is a bug or intended behavior.

2. Override hash and isEqual:

class ClassB : NSObject {
    let value : Int
    
    init(value : Int) {
        self.value = value
        super.init()
    }
    
    override var hash : Int {
        return value
    }
    
    override func isEqual(object: AnyObject?) -> Bool {
        if let other = object as? ClassB {
            return self.value == other.value
        } else {
            return false
        }
    }
}

For Swift 3, the definition of isEqual: changed to

override func isEqual(_ object: Any?) -> Bool { ... }

Now all results are as expected:

let b1 = ClassB(value: 13)
let b2 = ClassB(value: 13)

let nsSetB = NSSet(objects: b1, b2)
let swSetB = Set([b1, b2])

print(swSetB.count) // 1
print(nsSetB.count) // 1

let nsArrayB = NSArray(object: b1)
let swArrayB = [b1]

print(nsArrayB.indexOfObject(b2)) // 0
print(swArrayB.indexOf(b2)) // Optional(0)

Update: The behavior is documented in the book "Using Swift with Cocoa and Objective-C", under "Interacting with Objective-C API":

The default implementation of the == operator invokes the isEqual: method, and the default implementation of the === operator checks pointer equality. You should not override the equality or identity operators for types imported from Objective-C.

The base implementation of the isEqual: provided by the NSObject class is equivalent to an identity check by pointer equality. You can override isEqual: in a subclass to have Swift and Objective-C APIs determine equality based on the contents of objects rather than their identities.

The book is available in the Apple Book app.

It was also documented on Apple's website but was removed, and is still visible on the WebArchive snapshot of the page.

2
votes

For NSObject it is best to override hash and isEqual. It already conforms to Hashable and Equatable and has synthesized conformances for that which in turn invoke hash and isEqual. So since it is an NSObject, do it the ObjC way and override the values that also affect the ObjC hash value and equality.

class Identity: NSObject {

    let name: String
    let email: String

    init(name: String, email: String) {
        self.name = name
        self.email = email
    }

    override var hash: Int {
        var hasher = Hasher()
        hasher.combine(name)
        hasher.combine(email)
        return hasher.finalize()
    }

    override func isEqual(_ object: Any?) -> Bool {
        guard let other = object as? Identity else {
            return false
        }
        return name == other.name && email == other.email
    }
}
-2
votes

Implement Hashable, which also requires you to implement the == operator for your type. These are used for a lot of useful stuff in the Swift standard library like the indexOf function which only works on collections of a type that implements Equatable, or the Set<T> type which only works with types that implement Hashable.