有关如何正确覆盖object.GetHashCode()的一般build议和指导原则

根据MSDN ,哈希函数必须具有以下属性:

  1. 如果两个对象相等,每个对象的GetHashCode方法必须返回相同的值。 但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值。

  2. 对象的GetHashCode方法必须始终返回相同的哈希码,只要不会修改确定对象Equals方法的返回值的对象状态。 请注意,这仅适用于应用程序的当前执行,并且如果应用程序再次运行,则可以返回不同的散列码。

  3. 为了获得最佳性能,散列函数必须为所有input生成一个随机分布。


我不断发现自己在以下情况:我已经创build了一个类,实现了IEquatable<T>并重写IEquatable<T> object.Equals(object) 。 MSDN指出:

覆盖Equals的types也必须覆盖GetHashCode; 否则,Hashtable可能无法正常工作。

然后它通常会阻止我一点。 因为,你如何正确覆盖object.GetHashCode() ? 从来没有真正知道从哪里开始,这似乎是一个很大的陷阱。

在StackOverflow中,有很多与GetHashCode覆盖有关的问题,但是其中大部分似乎都是针对特定情况和特定问题。 所以,我想在这里得到一个很好的汇编。 一般build议和指导方针的概述。 该怎么做,不该做什么,常见的陷阱,从哪里开始等等。

我希望它特别针对C#,但是我认为它对于其他.NET语言也是如此(?)。


我想也许最好的办法是先用快速简短的答案为每个主题创build一个答案(如果可能的话,接近一行),然后可能会有更多的信息,并结束相关的问题,讨论,博客文章等。 ,如果有的话。 然后,我可以创build一个职位作为接受的答案(顶部),只是一个“目录”。 尽量保持简短。 不要只链接到其他问题和博客文章。 尝试把它们的本质,然后链接到源代码(尤其是源代码可能会消失。另外,请尝试编辑和改进答案,而不是创build很多非常相似的代码。

我不是一个很好的技术作家,但是我至less会尝试devise一些格式相似的答案,创build目录等等。我也会尝试在这里search一些相关的问题,这些,也许拉出我可以pipe理的精髓。 但是由于我对这个话题不太稳定,所以我会尽量远离:

目录

  • 我什么时候重写object.GetHashCode

  • 为什么我必须重写object.GetHashCode()?

  • 什么是在GetHashCode实现中看到的那些神奇数字?


我想覆盖的东西,但还没有:

  • 如何创build整数(如何将对象“转换”为int对我来说不是很明显)。
  • 哈希码的基础是什么字段。
    • 如果它只应该在不可变的领域,如果只有可变的领域呢?
  • 如何生成一个很好的随机分布。 (MSDN属性#3)
    • 这部分,似乎select了一个很好的魔术素数(已经看过17,23和397),但是你怎么select它,究竟是什么呢?
  • 如何确保散列码在整个对象生命周期内保持不变。 (MSDN属性#2)
    • 特别是当平等是基于可变的领域。 (MSDN属性#1)
  • 如何处理复杂types的字段(而不是内置的C#types )。
    • 复杂的对象和结构,数组,集合,列表,字典,genericstypes等
    • 例如,即使列表或字典可能是只读的,但这并不意味着它的内容。
  • 如何处理inheritance的类。
    • 如果你以某种方式将base.GetHashCode()并入你的哈希码?
  • 你能在技术上只是懒惰,并返回0? 将严重打破MSDN准则编号#3,但至less会确保#1和#2总是正确的:P
  • 常见的陷阱和陷阱。

什么是在GetHashCode实现中常见的那些神奇数字?

他们是素数。 素数用于创build哈希码,因为质数最大化了哈希码空间的使用。

具体来说,从小素数3开始,只考虑结果的低位低位:

  • 3 * 1 = 3 = 3(mod 8)= 0011
  • 3 * 2 = 6 = 6(mod 8)= 1010
  • 3 * 3 = 9 = 1(mod 8)= 0001
  • 3 * 4 = 12 = 4(mod 8)= 1000
  • 3 * 5 = 15 = 7(mod 8)= 1111
  • 3 * 6 = 18 = 2(mod 8)= 0010
  • 3 * 7 = 21 = 5(mod 8)= 1001
  • 3 * 8 = 24 = 0(mod 8)= 0000
  • 3 * 9 = 27 = 3(mod 8)= 0011

我们重新开始。 但是你会注意到,我们的素数的连续倍数在开始重复之前产生了我们的低位中的每一个可能的位的排列。 我们可以得到与任何素数和任何位数相同的效果,这使得素数最适合于生成近随机散列码。 在上面的例子中,我们通常会看到更大的素数而不是像3这样的小素数,因为对于我们的哈希码中更多的比特数,使用小素数的结果甚至不是伪随机的 – 它们只是一个简单的增加序列直到遇到溢出。 为了获得最优的随机性,除非可以保证系数不会很小,否则应该使用导致相当小的系数溢出的素数。

相关链接:

  • 为什么'397'用于ReSharper的GetHashCode覆盖?

查阅Eric Lippert的GetHashCode准则和规则

你应该覆盖它,只要你有一个有意义的衡量这种types的对象相等(即你覆盖等于)。 如果你知道这个对象不会因为任何原因被散列,那么你可以把它留下,但是你不可能事先知道这个。

散列应该仅基于用于定义相等性的对象的属性,因为被认为相等的两个对象应该具有相同的散列码。 一般来说,你通常会做这样的事情:

 public override int GetHashCode() { int mc = //magic constant, usually some prime return mc * prop1.GetHashCode() * prop2.GetHashCode * ... * propN.GetHashCode(); } 

我通常假设将这些值相乘会产生一个相当均匀的分布,假设每个属性的hashcode函数都是相同的,尽pipe这可能是错误的。 使用这种方法,如果对象的等式定义属性改变,那么哈希码也可能改变,这是可接受的给定定义#2在你的问题。 它也统一处理所有types。

您可以为所有实例返回相同的值,尽pipe这会使任何使用散列的algorithm(如dictionarys)非常慢 – 基本上所有实例都将散列到同一个存储桶,查找将变为O(n),而不是预期的O(1)。 这当然否定了使用这种结构进行查找的好处。

为什么我必须重写object.GetHashCode()

重写此方法非常重要,因为以下属性必须始终保持为真:

如果两个对象相等,每个对象的GetHashCode方法必须返回相同的值。

JaredPar在一篇关于实现平等的博客文章中指出,原因就在于此

许多类使用散列码对对象进行分类。 特别是散列表和字典倾向于将对象放置在基于散列码的桶中。 当检查对象是否已经在哈希表中时,它将首先在一个桶中查找它。 如果两个对象相等但是具有不同的散列码,则它们可以被放入不同的桶中,并且字典将无法查找该对象。

相关链接:

  • 我是否需要在新类中重写GetHashCode和Equals?
  • 我是否需要重写引用types的GetHashCode()?
  • 覆盖等于和GetHashCode问题
  • 为什么重写GetHashCode当Equals方法在C#中被重写时很重要?
  • 在VB中正确实现平等

A)如果你想使用值相等而不是默认的引用相等,你必须重写Equals和GetHashCode。 对于后者,如果两个对象引用都指向同一个对象实例,则两个对象引用的值相等。 对于前者,即使它们指的是不同的对象,它们的值也是相同的。 例如,您可能希望为Date,Money和Point对象使用值相等。

B)为了实现值相等,你必须重写Equals和GetHashCode。 两者都应该依赖于封装值的对象的字段。 例如,Date.Year,Date.Month和Date.Day; 或Money.Currency和Money.Amount; 或Point.X,Point.Y和Point.Z。 您还应该考虑覆盖运算符==,运算符!=,运算符<和运算符>。

C)散列码不必在整个对象生存期内保持不变。 然而,它必须保持不变,而它作为一个散列中的关键参与。 来自MSDN doco for Dictionary:“只要一个对象被用作Dictionary <(Of <(TKey,TValue>)>)中的一个键,它就不会以任何影响其散列值的方式进行改变。 如果您必须更改某个键的值,请从字典中删除条目,更改键值并replace该条目。

D)海事组织,如果你的价值对象本身是不变的,你将简化你的生活。

我什么时候重写object.GetHashCode()

正如MSDN所述:

覆盖Equals的types也必须覆盖GetHashCode; 否则,Hashtable可能无法正常工作。

相关链接:

  • 何时重写GetHashCode()?

什么字段的基础上的哈希代码? 如果它只应该在不可变的领域,如果只有可变的领域呢?

它不需要仅基于不可变的字段。 我会根据确定equals方法的结果的字段。

如何确保散列码在整个对象生命周期内保持不变。 (MSDN属性#2)特别是当平等是基于可变字段。 (MSDN属性#1)

你似乎误解了物业#2。 散列码在整个对象的生命周期中不需要保持不变。 只要确定equals方法结果的值不变,它只需要保持不变。 因此,逻辑上,您只是将哈希码作为这些值的基础。 那就不应该有问题。

 public override int GetHashCode() { return IntProp1 ^ IntProp2 ^ StrProp3.GetHashCode() ^ StrProp4.GetHashCode ^ CustomClassProp.GetHashCode; } 

在customClass的GetHasCode方法中执行相同的GetHasCode 。 奇迹般有效。