Dictionary如何在Swift中使用Equatable协议?

时间:2015-07-28 01:03:01

标签: swift dictionary hash-collision hashable equatable

为了解决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的例子中根本没有哈希冲突,但仍然在调用==。在我强迫哈希冲突的其他实验中(请参阅此问题的编辑历史记录),==似乎被称为随机次数。

这里发生了什么?

3 个答案:

答案 0 :(得分:11)

我在这里复制bugs.swift.org的答案。它讨论集合,但细节以同样的方式应用于字典。

  

在散列集合中,只要存储桶的数量小于键空间,就会发生冲突。当您在未指定最小容量的情况下创建新集时,该集可能只有一个存储桶,因此当您插入第二个元素时,会发生冲突。然后,插入方法将使用称为加载因子的东西来决定是否应该增长存储。如果存储增长,则必须将现有元素迁移到新的存储缓冲区。当您在插入4时看到对hashValue的所有额外调用时,那就是这样。

     

如果存储桶数量等于或大于元素数量,您仍然会看到对==的调用次数超出预期的原因与存储桶索引计算的实现细节有关。 hashValue的位被混合或&#34; shuffled&#34;在模运算之前。这是为了减少具有错误哈希算法的类型的过度冲突。

答案 1 :(得分:9)

嗯,有你的答案:

https://bugs.swift.org/browse/SR-3330?focusedCommentId=19980&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-19980

  

实际发生了什么:

     
      
  • 我们只在插入时散列一次值。
  •   
  • 我们不使用哈希来比较元素,只有==。使用哈希进行比较只有在存储哈希值时才合理,但是   这意味着每个字典的内存使用量更多。妥协   需要评估。
  •   
  • 我们尝试在评估Dictionary是否适合该元素之前插入元素。这是因为元素可能已经在   字典,在这种情况下,我们不再需要容量。
  •   
  • 当我们调整字典大小时,我们必须重新考虑所有内容,因为我们没有存储哈希。
  •   
     

所以你所看到的是:

     
      
  • 搜索关键字的一个哈希
  •   
  • some ==&#39; s(寻找空间)
  •   
  • 集合中每个元素的哈希值(调整大小)
  •   
  • 搜索关键字的一个哈希值(实际上完全是浪费,但考虑到它只在O重新分配后才发生,这不是什么大问题)
  •   
  • some ==&#39; s(在新缓冲区中搜索空格)
  •   

我们都完全错了。他们根本不使用哈希 - == - 来决定这是否是一个独特的密钥。然后在收集成长的情况下进行第二轮调用。

答案 2 :(得分:1)

虽然问题已经得到回答,但这些回答在评论中提出了一些其他问题。 @Suragch询问我是否将我的评论添加到新答案中,以帮助可能也会因EquatableHashable之间的关系而困惑的其他人。首先我要说的是,我对底层机制只有基本的了解,但是我会尽力解释我所知道的。

Equatable是一个非常简单的概念,对于这个协议的简洁定义,我们只需要看Swift文档即可:

  

相等:一种可以比较的值相等类型。

如果一个类型是相等的,我们可以用==比较两个实例。容易。

Hashable是另外一个故事。当我第一次在Swift文档中阅读该协议的定义时,我实际上大笑了:

  

可哈希:一种可以哈希到哈希器中以产生整数哈希值的类型。

enter image description here

如果这还没有为您解决,您并不孤单!而且无论如何,如果使用==来确定一个实例是否真正唯一(它必须在集合中或在字典中用作键),为什么我们根本需要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