我在一个集合中存储了少量自定义类的实例。我需要检查该集合中是否包含某个元素。匹配的条件必须是对象的 ID,而不是其内容。
为了简化起见,假设一个类将整数 var 作为唯一属性,以及该类的两个不同实例,都保存数字 1。
直接比较这些实例应该返回 true,但是当对第一个实例的引用存储在集合中时,如果集合包含对第二个实例的引用,则查询应返回 false。
因此,我使用对象的 ObjectIdentifier 来生成可哈希协议所需的哈希函数。
我的理解是,Swift Set 的 .contains 方法首先使用哈希值,在哈希冲突的情况下,使用等值方法作为回退。
但是在下面的代码中,可以在操场上运行,我得到了 randum 结果:
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 操场中运行上述代码并手动重新启动操场执行,则每次运行的最后一行都会得到不同的结果。期望的行为是每次都得到"假"。
那么,实现所描述的巴哈维的正确方法是什么?
简单地说,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 要求您在与集合交互之前手动包装对象。它是高性能的(因为结构不需要任何数组分配(,而且很可能结构是完全内联的,但它可能有点烦人。或者,你可以实现一个只包装一个Set<IdentityWrapper<T>>
的struct IdentitySet<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