当Equals方法被覆盖时,为什么重写GetHashCode非常重要?

鉴于下面的课

public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Which is preferred? return base.GetHashCode(); //return this.FooId.GetHashCode(); } } 

我已经重写了Equals方法,因为Foo表示Foo表的一行。 哪个是重写GetHashCode的首选方法?

为什么重写GetHashCode很重要?

是的,如果您的项目将被用作字典中的键或HashSet<T>等,这是非常重要的,因为这是使用(在没有自定义IEqualityComparer<T> )将项目分组到桶中。 如果两个项目的哈希码不匹配,他们可能永远不会被认为是相等的( Equals将永远不会被调用)。

GetHashCode()方法应该反映Equals逻辑; 规则是:

  • 如果两个事物相等( Equals(...) == true ),那么它们必须GetHashCode()返回相同的值
  • 如果GetHashCode()是相等的,那么它们就不一定是相同的。 这是一个冲突,等于是将被调用,看看它是否是一个真正的平等与否。

在这种情况下,它看起来像“ return FooId; ”是一个合适的GetHashCode()实现。 如果您正在testing多个属性,通常使用下面的代码将它们组合起来以减less对angular线冲突(即,使得new Foo(3,5)具有与new Foo(5,3)不同的散列码):

 int hash = 13; hash = (hash * 7) + field1.GetHashCode(); hash = (hash * 7) + field2.GetHashCode(); ... return hash; 

哦 – 为了方便起见,当重写EqualsGetHashCode时,您可能还会考虑提供==!=运算符。


当你发现这个错误的时候,会发生什么情况。

实际上很难正确实现GetHashCode() ,因为除了Marc已经提到的规则之外,哈希码在对象的生命周期内不应该改变。 因此,用于计算哈希码的字段必须是不可变的。

当我使用NHibernate时,终于find了解决这个问题的办法。 我的方法是从对象的ID中计算哈希码。 这个ID只能通过构造函数来设置,所以如果你想改变ID,这是不太可能的,你必须创build一个新的对象,它有一个新的ID,因此一个新的哈希码。 这种方法最适合于GUID,因为您可以提供一个随机生成ID的无参数构造函数。

通过覆盖Equals,你基本上说明你是一个更好地知道如何比较给定types的两个实例的人,所以你很可能是提供最好的哈希码的最佳人选。

这是ReSharper如何为你写一个GetHashCode()函数的例子:

  public override int GetHashCode() { unchecked { var result = 0; result = (result * 397) ^ m_someVar1; result = (result * 397) ^ m_someVar2; result = (result * 397) ^ m_someVar3; result = (result * 397) ^ m_someVar4; return result; } } 

正如你所看到的,它只是试图根据类中的所有字段来猜测一个好的散列码,但是由于你知道你的对象的域或值范围,你仍然可以提供一个更好的。

重写Equals()时,请不要忘记检查obj参数。 而且还比较types。

 public override bool Equals(object obj) { if (obj == null || GetType() != obj.GetType()) return false; Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } 

原因是: Equals必须返回false与null比较。 另请参阅http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

怎么样:

 public override int GetHashCode() { return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode(); } 

假设性能不是问题:)

这是因为框架要求两个相同的对象必须具有相同的哈希码。 如果重写equals方法对两个对象进行特殊比较,并且这两个对象被方法视为相同,那么这两个对象的哈希码也必须相同。 (字典和哈希表依赖于这个原则)。

我们有两个问题需要解决。

  1. 如果可以更改对象中的任何字段,则无法提供明智的GetHashCode() 。 也永远不会在依赖于GetHashCode()的集合中使用对象。 所以实现GetHashCode()的代价通常是不值得的,否则这是不可能的。

  2. 如果有人把你的对象放在一个调用GetHashCode()的集合中,并且你没有使GetHashCode()以正确的方式运行,那么你可能会花费数天的时间来追踪这个问题。

所以默认情况下我这样做。

 public class Foo { public int FooId { get; set; } public string FooName { get; set; } public override bool Equals(object obj) { Foo fooItem = obj as Foo; return fooItem.FooId == this.FooId; } public override int GetHashCode() { // Some comment to explain if there is a real problem with providing GetHashCode() // or if I just don't see a need for it for the given class throw new Exception("Sorry I don't know what GetHashCode should do for this class"); } } 

散列码用于基于散列的集合,如Dictionary,Hashtable,HashSet等。此代码的目的是通过将特定对象放入特定的组(bucket)来快速预先sorting特定的对象。 当你需要从哈希集合中找回这个对象时,这个预先sorting有助于寻找这个对象,因为代码必须在一个存储桶中search你的对象,而不是在它所包含的所有对象中search你的对象。 哈希码的更好分配(更好的唯一性)更快的检索。 在理想的情况下,每个对象都有一个唯一的哈希码,find它是一个O(1)操作。 在大多数情况下,它接近O(1)。

只是添加上面的答案:

如果你不覆盖Equals,那么缺省的行为就是比较对象的引用。 这同样适用于hashcode – 默认implmentation通常是基于引用的内存地址。 因为你没有覆盖Equals,这意味着正确的行为是比较你在Equals上实现而不是引用,所以你应该对hashcode做同样的事情。

您的类的客户端会希望哈希码与equals方法具有相似的逻辑,例如,使用IEqualityComparer的linq方法首先比较哈希码,只有在它们相等的情况下,才会比较可能更昂贵的Equals()方法运行,如果我们没有实现哈希码,等于对象可能会有不同的哈希码(因为它们具有不同的内存地址),并被错误地判定为不相等(Equals()甚至不会被命中)。

另外,除了在字典中使用它的问题,你可能无法find你的对象(因为它是由一个散列码插入的,当你查找它的时候,默认的散列码可能是不同的,Equals()甚至不会被称为,就像Marc Gravell在他的回答中解释的那样,你还会介绍违反字典或哈希集的概念,它们不应该允许使用相同的键 – 你已经声明这些对象基本上是相同的,不要把它们当作一个假设有唯一键的数据结构上的不同键,但是由于它们具有不同的散列码,“相同”键将被插入为不同的键。

这不一定重要; 它取决于你的集合的大小和你的性能要求,以及你的类是否会被用在你可能不知道性能要求的库中。 我经常知道我的collections大小不是很大,而且我的时间比创build一个完美的哈希码所获得的几微妙的性能更有价值。 所以(通过编译器摆脱恼人的警告)我只是使用:

  public override int GetHashCode() { return base.GetHashCode(); } 

(当然,我也可以使用#pragma来closures警告,但我更喜欢这种方式。)

当然,你所处的位置,你确实需要的performance,比其他人提到的所有问题都适用。 最重要的是 ,否则当从哈希集合或字典中检索项目时会得到错误的结果: 哈希代码不能随着对象的生存时间而变化 (更准确地说,在需要哈希代码的时候字典中的一个关键字):例如,以下内容是错误的,因为Value是公共的,所以在实例的生命期内可以在类的外部进行更改,因此您不得将其用作哈希码的基础:

 class A { public int Value; public override int GetHashCode() { return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time } } 

另一方面,如果价值不能改变,可以使用:

 class A { public readonly int Value; public override int GetHashCode() { return Value.GetHashCode(); //OK Value is read-only and can't be changed during the instance's life time } } 

这是我的理解,原来的GetHashCode()返回对象的内存地址,所以如果你想比较两个不同的对象,这是必要的。

编辑:这是不正确的,原始的GetHashCode()方法不能保证2值的相等。 尽pipe相同的对象返回相同的哈希码。

下面使用reflection似乎是一个更好的select,考虑公共财产,因为你不必担心增加/删除属性(虽然不是很常见的情况)。 我发现这个performance也更好。(比较使用Diagonistics秒表的时间)。

  public int getHashCode() { PropertyInfo[] theProperties = this.GetType().GetProperties(); int hash = 31; foreach (PropertyInfo info in theProperties) { if (info != null) { var value = info.GetValue(this,null); if(value != null) unchecked { hash = 29 * hash ^ value.GetHashCode(); } } } return hash; }