Swift 中的 NSObject 子类:hash vs hashValue,isEqual vs ==

Swift 中的 NSObject 子类:hash vs hashValue,isEqual vs ==

问题描述:

在 Swift 中子类化 NSObject 时,你应该覆盖 hash 还是实现 Hashable?

When subclassing NSObject in Swift, should you override hash or implement Hashable?

此外,您应该覆盖 isEqual: 还是实现 == 运算符?

Also, should you override isEqual: or implement the == operator?

NSObject 已经符合 Hashable 协议:

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

我找不到官方参考,但似乎hashValueNSObjectProtocol 调用 hash 方法,== 调用isEqual: 方法(来自同一协议).见更新答案结束!

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!

对于 NSObject 子类,正确的方法似乎是覆盖 hashisEqual:,这是一个实验证明:

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

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

如您所见,NSSetSet 都将对象视为不同的对象.这不是想要的结果.数组也有意想不到的结果:

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

设置断点或添加调试输出显示被覆盖的== 运算符永远不会被调用.不知道是bug还是预期行为.

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.

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
        }
    }
}

对于 Swift 3,isEqual: 的定义改为

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)


更新:该行为记录在使用 Swift 与 Cocoa 和 Objective-C"一书中的与 Objective-C API 交互"下:


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

== 运算符的默认实现调用 isEqual: 方法,=== 运算符的默认实现检查指针平等.对于从 Objective-C 导入的类型,您不应覆盖相等或标识运算符.

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.

NSObject 类提供的 isEqual: 的基本实现等效于通过指针相等性进行的身份检查.您可以在子类中覆盖 isEqual:,让 Swift 和 Objective-C API 根据对象的内容而不是它们的身份来确定相等性.

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.

这本书可在 Apple Book 应用中找到.

The book is available in the Apple Book app.

它也记录在 Apple 的网站上但已被删除,并且在 WebArchive 快照页面.

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