为了解决this question,我一直在玩一个实现Hashable Protocol的自定义结构。我试图查看等效运算符重载(==
)被调用的次数,具体取决于填充Dictionary
时是否存在哈希冲突。
更新
@matt编写了一个更清晰的自定义结构示例,该结构实现了Hashable协议,并显示了调用hashValue
和==
的频率。我在下面复制his code。要查看我的原始示例,请查看edit history。
struct S : Hashable {
static func ==(lhs:S,rhs:S) -> Bool {
print("called == for", lhs.id, rhs.id)
return lhs.id == rhs.id
}
let id : Int
var hashValue : Int {
print("called hashValue for", self.id)
return self.id
}
init(_ id:Int) {self.id = id}
}
var s = Set<S>()
for i in 1...5 {
print("inserting", i)
s.insert(S(i))
}
这会产生结果:
/*
inserting 1
called hashValue for 1
inserting 2
called hashValue for 2
called == for 1 2
called hashValue for 1
called hashValue for 2
inserting 3
called hashValue for 3
inserting 4
called hashValue for 4
called == for 3 4
called == for 1 4
called hashValue for 2
called hashValue for 3
called hashValue for 1
called hashValue for 4
called == for 3 4
called == for 1 4
inserting 5
called hashValue for 5
*/
由于Hashable使用Equatable来区分哈希冲突(无论如何我都假设),我希望只有在发生哈希冲突时才会调用func ==()
。但是,在@ matt的例子中根本没有哈希冲突,但仍然在调用==
。在我强迫哈希冲突的其他实验中(请参阅此问题的编辑历史记录),==
似乎被称为随机次数。
这里发生了什么?
答案 0 :(得分:11)
我在这里复制bugs.swift.org的答案。它讨论集合,但细节以同样的方式应用于字典。
在散列集合中,只要存储桶的数量小于键空间,就会发生冲突。当您在未指定最小容量的情况下创建新集时,该集可能只有一个存储桶,因此当您插入第二个元素时,会发生冲突。然后,插入方法将使用称为加载因子的东西来决定是否应该增长存储。如果存储增长,则必须将现有元素迁移到新的存储缓冲区。当您在插入4时看到对hashValue的所有额外调用时,那就是这样。
如果存储桶数量等于或大于元素数量,您仍然会看到对==的调用次数超出预期的原因与存储桶索引计算的实现细节有关。 hashValue的位被混合或&#34; shuffled&#34;在模运算之前。这是为了减少具有错误哈希算法的类型的过度冲突。
答案 1 :(得分:9)
嗯,有你的答案:
实际发生了什么:
- 我们只在插入时散列一次值。
- 我们不使用哈希来比较元素,只有==。使用哈希进行比较只有在存储哈希值时才合理,但是 这意味着每个字典的内存使用量更多。妥协 需要评估。
- 我们尝试在评估Dictionary是否适合该元素之前插入元素。这是因为元素可能已经在 字典,在这种情况下,我们不再需要容量。
- 当我们调整字典大小时,我们必须重新考虑所有内容,因为我们没有存储哈希。
所以你所看到的是:
- 搜索关键字的一个哈希
- some ==&#39; s(寻找空间)
- 集合中每个元素的哈希值(调整大小)
- 搜索关键字的一个哈希值(实际上完全是浪费,但考虑到它只在O重新分配后才发生,这不是什么大问题)
- some ==&#39; s(在新缓冲区中搜索空格)
我们都完全错了。他们根本不使用哈希 - 仅 ==
- 来决定这是否是一个独特的密钥。然后在收集成长的情况下进行第二轮调用。
答案 2 :(得分:1)
虽然问题已经得到回答,但这些回答在评论中提出了一些其他问题。 @Suragch询问我是否将我的评论添加到新答案中,以帮助可能也会因Equatable
和Hashable
之间的关系而困惑的其他人。首先我要说的是,我对底层机制只有基本的了解,但是我会尽力解释我所知道的。
Equatable
是一个非常简单的概念,对于这个协议的简洁定义,我们只需要看Swift文档即可:
相等:一种可以比较的值相等类型。
如果一个类型是相等的,我们可以用==
比较两个实例。容易。
Hashable
是另外一个故事。当我第一次在Swift文档中阅读该协议的定义时,我实际上大笑了:
可哈希:一种可以哈希到哈希器中以产生整数哈希值的类型。
如果这还没有为您解决,您并不孤单!而且无论如何,如果使用==
来确定一个实例是否真正唯一(它必须在集合中或在字典中用作键),为什么我们根本需要Hashable
? (这是@Suragch在评论中提出的问题。)
这个问题涉及诸如集合和字典之类的哈希集合(或哈希表)的基本性质。考虑一下为什么首先要在数组上选择字典。当您需要通过已知索引以外的其他内容查找或引用实例时,可以选择字典,对吗?与数组不同,字典的元素没有按顺序编号,这使查找内容变得更加困难。如果我们只拥有==
,则必须逐一遍历字典的元素,并且随着字典大小的增加,该元素将变得越来越慢。
这是哈希函数的神奇之处。哈希函数(或哈希器)将唯一键作为参数,并返回元素的地址。您如何确定它将返回正确的地址?因为它与最初用于设置该地址的功能相同!创建字典时,它会使用每个键(或更确切地说,每个键的唯一标识属性),根据某个秘密公式将它们混搭在一起,并为每个键吐出一个数字(哈希值),然后从这些数字中得出每个元素的新索引。稍后,当您要查找这些元素之一时,hasher获得相同的参数,因此它返回相同的值。而且,由于您所做的只是调用一个函数,因此无论集合有多大,都不会涉及迭代,并且结果很快。
但是有一个陷阱。没有哈希器是完美的。即使您为它提供了唯一的参数,它有时也可能为两个完全不同的元素(哈希冲突)返回相同的哈希值。发生这种情况时,它需要检查两个元素是否确实相同,并且当然可以通过调用==
来做到这一点。
但是在您的示例中,您直接操作了hashValue
(这是人们在hash(into:)
出现之前所做的事情!),它仍然称为==
!我的意思是,从理论上讲,由于没有任何碰撞,因此不需要这样做。但是答案在comment quoted by robinkunde中:
在散列集合中,只要存储桶的数量小于键空间,就会发生冲突
虽然通常来说,我们不必担心Swift内置的hasher函数的实现细节,但是这个特殊的细节很重要……在幕后,hasher需要另一个参数,那就是集合的大小。如果大小发生变化(当您遍历一个范围并将新元素插入到集合中时会反复执行),散列器可以尝试推入比已索引的插槽(或存储桶)更多的元素,并且会发生冲突,或者它需要使用足够的内存从头开始重新哈希所有内容,以便为每个元素提供唯一索引。 comment quoted by matt说:
我们尝试在评估Dictionary是否适合该元素之前插入该元素。这是因为该元素可能已经在字典中,在这种情况下,我们不再需要任何容量。
因此,这是我对哈希集合的简单解释,哈希函数与==
方法之间的关系以及意外行为的原因的尝试。但这一切又给我提出了一个问题……为什么我们需要手动声明Hashable
?苹果公司难道没有采用某种算法来为所有Equatable类型合成Hashable一致性吗?我的意思是,hash(into:)
documentation说:
用于散列的组件必须与组件相同 在您类型的==运算符实现中进行了比较。
如果组件需要相同,那么Swift不能仅从Equatable
的实现中推断出我们的意图吗?对于那些不想对细节进行更多控制的人,我不确定为什么它不能提供这种便利(类似于它提供默认初始化程序的方式)。也许有一天Swift会提供这个?尽管目前,他们仍将它们作为单独的关注点,Hashable
继承自Equatable
。