NSObject subclass in Swift: hash vs hashValue, isEqual vs ==

Spread the love

Question Description

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

Practice As Follows

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 now documented in
Interacting with Objective-C APIs
in the “Using Swift with Cocoa and Objective-C” reference:

The NSObject class only performs an identity comparison, so you should implement your own isEqual: method in classes that derive from the NSObject class.

As part of implementing equality for your class, be sure to implement the hash property according to the rules in Object comparison.

Leave a Comment