Swift:将类的 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?

查看:67
本文介绍了Swift:将类的 ObjectID 用于可哈希协议会导致 set.contains 方法中的随机行为.代码有什么问题?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a small number of instances of a custom class stored in a set. I need to check if a certain element is contained in that set. The criteria for a match must be the object's ID, not its content.

为了简单起见,假设一个类的唯一属性是一个整数 var,以及该类的两个不同实例,都保存数字 1.

For simplification assume a class with an integer var as the only property, and two different instances of that class, both holding the number 1.

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

Directly comparing those instances should return true, but when a reference to the first is stored in the set, a query if the set contains a reference to the second one should return false.

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

Therefore I use the ObjectIdentifier of the object to generate the hash function required by the hashable protocol.

我的理解是 Swift Set 的 .contains 方法首先使用哈希值,如果发生哈希冲突,则使用 equatable 方法作为后备.

It is my understanding that the .contains method of a Swift Set uses the hash value first, and in case of hash collisions the equatable method is used as a fallback.

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

But in the following code, which can run in a playground, I get randum results:

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 执行,则每次运行的最后一行都会产生不同的结果.期望的行为是每次都得到假".

If you run the above code in an XCode Playground and manually restart playground execution this gives different results for the last line on each run. The desired behaviour is to get "false" every time.

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

So what would be the correct way to achieve the described bahaviour?

推荐答案

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

Simply put, Set relies on func hash(into hasher: inout Hasher) and ==. It is invalid to have an unmatched pair of these. In your case, your equality is value-based (dependant upon self.number), whereas your hash is identity based. This isn't legal.

您的 mySet.contains(secondNumber1) 行失败,因为 secondNumber2 可能与 number1 发生哈希冲突.是否发生碰撞是不确定的,因为 Swift 使用随机种子来防御哈希泛滥 DDoS 攻击.如果确实发生了哈希冲突,那么您的相等运算符 (==) 会错误地将 number1 识别为与 secondNumber1

Your mySet.contains(secondNumber1) line is failing because secondNumber2 might have a hash collision with number1. Whether a collision occurs or not is undefined, because Swift uses a random seed to defend against hash-flood DDoS attacks. If a hash collision does occur, then your equality operator (==) falsely identifies as number1 as a match for secondNumber1

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

Instead, what you could do is implement a wrapper struct that implements equality and hashing based on an object's identity. The object itself could have its own value-based equality and hash, for other purposes.

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 IdentitySet<T>,它只包装一个 Set<IdentityWrapper<T>>,它隐藏了包装代码.

Using the IdentityWrapper in a set requires you to manually wrap objects before interacting with the set. It's performant (since struct don't need any array allocation), and most likely the struct is entirely inlined anyway, but it can be a little annoying. Optionally, you could implement a struct IdentitySet<T> which just wraps a Set<IdentityWrapper<T>>, which tucks away the wrapping code.

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

这篇关于Swift:将类的 ObjectID 用于可哈希协议会导致 set.contains 方法中的随机行为.代码有什么问题?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆