Swift:将 class 的 ObjectID 用于可散列协议会导致 set.contains 方法中的随机行为。代码有什么问题?

Swift: Using ObjectID of class for hashable protocol results in random behaviour in set.contains method. What is wrong with the code?

我在一组中存储了少量自定义 class 实例。我需要检查该集合中是否包含某个元素。匹配条件必须是对象的 ID,而不是其内容。

为简化起见,假设 class 具有整数变量作为唯一的 属性,以及该 class 的两个不同实例,均包含数字 1。

直接比较这些实例应该 return 为真,但是当对第一个实例的引用存储在集合中时,查询集合是否包含对第二个实例的引用应该 return 为假。

因此我使用对象的ObjectIdentifier来生成hashable协议需要的hash函数

据我了解,Swift Set 的 .contains 方法首先使用散列值,如果发生散列冲突,则使用 equatable 方法作为回退。

但在下面的代码中,可以 运行 在操场上,我得到随机结果:

class MyClass: Hashable {
    var number: Int
    init(_ number: Int) {
        self.number = number
    }
    static func == (lhs: MyClass, rhs: MyClass) -> Bool {
        return lhs.number == rhs.number
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}

var mySet: Set<MyClass> = []

let number1 = MyClass(1)
let secondNumber1 = MyClass(1)

number1 == secondNumber1        // true: integer values are equal, so are the wrapping classes
number1 === secondNumber1       // false: two different instances

mySet.insert(number1)

mySet.contains(number1)         // true
mySet.contains(secondNumber1)   // should be false but randomly changes between runs

如果您在 XCode Playground 中 运行 上面的代码并手动重新启动 playground 执行,这会在每个 运行 的最后一行给出不同的结果。期望的行为是每次都获得 "false"。

那么实现上述行为的正确方法是什么?

简单地说,Set依赖于func hash(into hasher: inout Hasher)==。一对不匹配的是无效的。在您的情况下,您的平等是基于价值的(取决于 self.number),而您的散列是基于身份的。这是不合法的。

您的 mySet.contains(secondNumber1) 行失败,因为 secondNumber2 可能与 number1 发生哈希冲突。是否发生碰撞是不确定的,因为Swift uses a random seed to defend against hash-flood DDoS attacks。如果确实发生哈希冲突,那么您的相等运算符 (==) 会错误地将 number1 标识为 secondNumber1

的匹配项

相反,您可以做的是实现一个包装器结构,它根据对象的身份实现相等性和散列。对象本身可以有自己的基于值的相等性和散列,用于其他目的。

struct IdentityWrapper<T: AnyObject> {
    let object: T

    init(_ object: T) { self.object = object }
}

extension IdentityWrapper: Equatable {
    static func == (lhs: IdentityWrapper, rhs: IdentityWrapper) -> Bool {
        return lhs.object === rhs.object
    }
}

extension IdentityWrapper: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self.object))
    }
}

在集合中使用 IdentityWrapper 需要您在与集合交互之前手动包装对象。它是高性能的(因为 struct 不需要任何数组分配),而且很可能 struct 无论如何都是完全内联的,但它可能有点烦人。或者,您可以实现一个 struct IdentitySet<T>,它只包装一个 Set<IdentityWrapper<T>>,它隐藏包装代码。

class MyClass: Hashable {
    var number: Int

    init(_ number: Int) {
        self.number = number
    }

    // Value-based equality
    static func == (lhs: MyClass, rhs: MyClass) -> Bool {
        return lhs.number == rhs.number
    }

    // Value-based hashing
    func hash(into hasher: inout Hasher) {
        hasher.combine(self.number)
    }
}

var mySet: Set<IdentityWrapper<MyClass>> = []

let number1 = MyClass(1)
let secondNumber1 = MyClass(1)

number1 == secondNumber1        // true: integer values are equal, so are the wrapping classes
number1 === secondNumber1       // false: two different instances

mySet.insert(IdentityWrapper(number1))

print(mySet.contains(IdentityWrapper(number1))) // true
print(mySet.contains(IdentityWrapper(secondNumber1))) // false