我的自定义结构实现了Hashable Protocol。但是,当在Dictionary
中插入密钥时发生哈希冲突时,它们不会自动处理。我该如何克服这个问题?
我之前曾问过这个问题How to implement the Hashable Protocol in Swift for an Int array (a custom string struct)。后来我添加了my own answer,这似乎正在发挥作用。
但是,最近我在使用hashValue
时发现了Dictionary
碰撞的微妙问题。
我尽可能地将代码简化为以下示例。
自定义结构
struct MyStructure: Hashable {
var id: Int
init(id: Int) {
self.id = id
}
var hashValue: Int {
get {
// contrived to produce a hashValue collision for id=1 and id=2
if id == 1 {
return 2
}
return id
}
}
}
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.hashValue == rhs.hashValue
}
注意全局函数重载相等运算符(==)以符合Hashable Protocol所需的Equatable Protocol。
微妙的词典键问题
如果我使用Dictionary
作为密钥
MyStructure
var dictionary = [MyStructure : String]()
let ok = MyStructure(id: 0) // hashValue = 0
let collision1 = MyStructure(id: 1) // hashValue = 2
let collision2 = MyStructure(id: 2) // hashValue = 2
dictionary[ok] = "some text"
dictionary[collision1] = "other text"
dictionary[collision2] = "more text"
print(dictionary) // [MyStructure(id: 2): more text, MyStructure(id: 0): some text]
print(dictionary.count) // 2
相等的哈希值会导致collision1
密钥被collision2
密钥覆盖。没有警告。如果这样的碰撞只发生在一个有100个键的字典中,那么它很容易被遗漏。 (我花了很长时间才注意到这个问题。)
字典文字的明显问题
如果我用字典文字重复这一点,问题会变得更加明显,因为会抛出致命的错误。
let ok = MyStructure(id: 0) // hashValue = 0
let collision1 = MyStructure(id: 1) // hashValue = 2
let collision2 = MyStructure(id: 2) // hashValue = 2
let dictionaryLiteral = [
ok : "some text",
collision1 : "other text",
collision2 : "more text"
]
// fatal error: Dictionary literal contains duplicate keys
我的印象是hashValue
无需始终返回唯一值。例如,Mattt Thompson says,
关于实现自定义哈希的最常见误解之一 函数来自......认为哈希值必须是不同的。
受尊重的SO用户@Gaffa says是一种处理哈希冲突的方法是
将哈希码视为非唯一,并使用相等比较器 实际数据以确定唯一性。
在我看来,在撰写本文时,问题Do swift hashable protocol hash functions need to return unique values?尚未得到充分的回答。
在阅读Swift Dictionary
问题How are hash collisions handled?后,我认为Swift会自动处理与Dictionary
的哈希冲突。但是,如果我使用自定义类或结构,显然不是这样。
This comment让我觉得答案在于如何实现Equatable协议,但我不确定如何更改它。
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.hashValue == rhs.hashValue
}
是否为每个字典键查找调用此函数或仅在存在哈希冲突时调用此函数? (更新:见this question)
在发生哈希冲突时(以及仅当)发生哈希冲突时,我该怎么做才能确定唯一性?
答案 0 :(得分:8)
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.hashValue == rhs.hashValue
}
注意全局函数重载等于运算符(==)以符合Hasable Protocol所需的Equatable Protocol。
您的问题是相等实施不正确。
哈希表(例如Swift词典或集合)需要单独的相等和哈希实现。
hash 让您接近您正在寻找的对象; equality 为您提供您正在寻找的确切对象。
您的代码对 hash 和 equality 使用相同的实现,这将保证发生冲突。
要解决此问题,请实现 equality 以匹配确切的对象值(但是您的模型定义了相等性)。 E.g:
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.id == rhs.id
}
答案 1 :(得分:3)
我认为你拥有所需的所有部分 - 你只需将它们组合在一起。你有很多很好的资源。
哈希碰撞没问题。如果发生哈希冲突,则将检查对象是否相等(仅针对具有匹配哈希的对象)。出于这个原因,对象' Equatable
一致性需要基于hashValue
之外的其他内容,除非您确定哈希值不会发生冲突。
这是符合Hashable
的对象必须符合Equatable
的确切原因。当哈希没有削减它时,Swift需要一个更具域特定的比较方法。
在同一个NSHipster article中,您可以看到Mattt在他的示例isEqual:
类中如何实现hash
与Person
的对比。具体来说,他有一个isEqualToPerson:
方法来检查一个人的其他属性(生日,全名)以确定是否相等。
- (BOOL)isEqualToPerson:(Person *)person {
if (!person) {
return NO;
}
BOOL haveEqualNames = (!self.name && !person.name) || [self.name isEqualToString:person.name];
BOOL haveEqualBirthdays = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday];
return haveEqualNames && haveEqualBirthdays;
}
在检查相等性时,他不使用哈希值 - 他使用特定于他的人类的属性。
同样,Swift不允许您简单地使用Hashable
对象作为字典键 - 隐式地,通过协议继承 - 键也必须符合Equatable
。对于标准库Swift类型,已经考虑过了,但是对于自定义类型和类,您必须创建自己的==
实现。这就是Swift不会自动处理自定义类型的字典冲突的原因 - 您必须自己实现Equatable
!
作为一种离别思想,Mattt还指出,您通常可以只进行身份检查,以确保您的两个对象位于不同的内存地址,从而确定不同的对象。在Swift中,这就像这样:
if person1 === person2 {
// ...
}
此处无法保证person1
和person2
具有不同的属性,只是它们占用内存中的单独空间。相反,在早期的isEqualToPerson:
方法中,不能保证具有相同名称和生日的两个人实际上是同一个人。因此,您必须考虑对您特定对象类型有意义的内容。同样,Swift没有在自定义类型上为您实现Equatable
的另一个原因。
答案 2 :(得分:3)
相等的哈希值会导致collision1键被覆盖 collision2键。没有警告。如果只是这样的碰撞 在一个有100个键的字典中发生过一次,那么很容易就可以了 错过。
哈希碰撞与它无关。 (哈希冲突永远不会影响结果,只影响性能。)它完全按照文档记录工作。
Dictionary
操作适用于相等(==
)个键。字典不包含重复键(表示相同的键)。使用键设置值时,它会覆盖包含相等键的任何条目。当你得到带有下标的条目时,它会找到一个值与你给出的东西相等但不一定相同的值。等等。
collision1
和collision2
相等(==
),具体取决于您定义==
运算符的方式。因此,使用键collision2
设置条目必须使用键collision1
覆盖任何条目。
P.S。同样适用于其他语言的词典。例如,在Cocoa中,NSDictionary
不允许重复键,即isEqual:
的键。在Java中,Map
不允许重复密钥,即.equals()
的密钥。
答案 3 :(得分:1)
您可以在此页面的答案和this答案中看到我的评论。我认为所有的答案仍然以非常令人困惑的方式写出来。 子>
tl; dr 0)您不需要在hashValues之间编写实现isEqual ie ==。 1)仅提供/返回hashValue
。 2)像往常一样实施Equatable
0)要符合hashable
,您必须拥有名为hashValue
的计算值,并为其指定适当的值。 与 equatable
协议不同,hashValues的比较已经已经。你不要需要写:
func ==(lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.hashValue == rhs.hashValue
// Snippet A
}
1)然后它使用hashValue
检查是否对于该hashValue的索引(通过其模数对数组的计数计算)所查找的密钥是否存在。它在该索引的键/值对数组中查找。
2) 然而作为失败保护,即如果 匹配哈希值,您将回退到常规==
函数。 (逻辑上你需要它是因为故障安全。但你也需要它,因为Hashable协议符合Equatable,因此你必须为==
编写一个实现。否则你会得到一个编译器错误)
func == (lhs: MyStructure, rhs: MyStructure) -> Bool {
return lhs.id == rhs.id
//Snippet B
}
<强>结论:强>
您必须包含代码段B,排除代码段A,同时还具有hashValue
属性