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

问题描述

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

为简化起见,假设一个具有整数 var 作为唯一属性的类,以及该类的两个不同实例,都持有数字 1。

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

因此我使用对象的 ObjectIdentifier 来生成可散列协议所需的散列函数。

据我了解,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 执行,则每次运行的最后一行都会产生不同的结果。期望的行为是每次都得到“假”。

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

标签: swiftsethashable

解决方案


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

您的mySet.contains(secondNumber1)线路失败,因为secondNumber2可能与number1. 是否发生冲突是不确定的,因为Swift 使用随机种子来防御哈希泛洪 DDoS 攻击。如果确实发生了哈希冲突,那么您的等式运算符 ( ==) 会错误地将其识别为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 不需要任何数组分配),而且很可能该结构是完全内联的,但它可能有点烦人。或者,您可以实现 a ,它只struct IdentitySet<T>包装 a 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

推荐阅读