当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;
if (fooItem == null){return false;}
return fooItem.FooId == this.FooId;}
public override int GetHashCode(){// Which is preferred?
return base.GetHashCode();
//return this.FooId.GetHashCode();}}

我重写了Equals方法,因为Foo代表Foos表的一行。重写GetHashCode的首选方法是什么?

为什么重写GetHashCode很重要?

443489 次浏览

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

获取HashCode()方法名方法应该反映Equals逻辑;规则是:

  • 如果两个事物相等(Equals(...) == true),则它们必须GetHashCode()返回相同的值
  • 如果GetHashCode()相等,则没有必须相同;这是碰撞,Equals将被调用以查看它是否是真正的相等。

在这种情况下,看起来“return FooId;”是合适的GetHashCode()实现。如果您正在测试多个属性,通常使用如下代码将它们组合在一起,以减少对角线冲突(即new Foo(3,5)new Foo(5,3)有不同的哈希代码):

在现代框架中,HashCode类型具有帮助您从多个值创建哈希码的方法;在较旧的框架上,您需要没有,例如:

unchecked // only needed if you're compiling with arithmetic checks enabled{ // (the default compiler behaviour is *disabled*, so most folks won't need this)int hash = 13;hash = (hash * 7) + field1.GetHashCode();hash = (hash * 7) + field2.GetHashCode();...return hash;}

哦-为了方便起见,您还可以考虑在覆盖EqualsGetHashCode时提供==!=运算符。


当你犯这个错误时会发生什么的演示是这里

是因为框架要求相同的两个对象必须具有相同的hashcode,如果重写equals方法做两个对象的特殊比较,且方法认为两个对象相同,那么两个对象的hash code也必须相同(字典和哈希表就是靠这个原理)。

通过重写Equals,您基本上是在说明您更了解如何比较给定类型的两个实例。

您可以在下面看到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;}}

如您所见,它只是尝试根据类中的所有字段猜测一个好的哈希代码,但如果您知道对象的域或值范围,您仍然可以提供更好的哈希代码。

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

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

不如:

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

假设性能不是问题:)

这不一定重要;这取决于你的集合的大小和你的性能要求,以及你的类是否会在你可能不知道性能要求的库中使用。我经常知道我的集合大小不是很大,我的时间比通过创建完美的哈希代码获得的几微秒性能更有价值;所以(为了摆脱编译器令人讨厌的警告)我简单地使用:

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

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

最重要-否则在从哈希集或字典中检索项目时会得到错误的结果:哈希码不能随对象的生命周期而变化(更准确地说,在需要哈希码的时候,例如在字典中作为键时):例如,以下是错误的,因为Value是公共的,因此可以在实例的生命周期内从外部更改到类,所以你不能将其用作哈希码的基础:

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

另一方面,如果Value无法更改,则可以使用:

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}}

重写Equals()时,请不要忘记检查obj参数对null。并比较类型。

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

这样做的原因是:与null相比,Equals必须返回false。另见http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

哈希代码用于基于哈希的集合,如字典、哈希表、哈希集等。该代码的目的是通过将特定对象放入特定组(桶)来非常快速地对特定对象进行预排序。当你需要从哈希集合中检索它时,这种预排序对找到该对象有很大帮助,因为代码必须在一个桶中搜索你的对象,而不是在它包含的所有对象中搜索。哈希代码的更好分布(更好的唯一性)检索速度更快。在理想情况下,每个对象都有一个唯一的哈希代码,找到它是一个O(1)操作。在大多数情况下,它接近O(1)。

我的理解是原始的GetHashCode()返回对象的内存地址,因此如果您希望比较两个不同的对象,则必须覆盖它。

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

我们有两个问题要处理。

  1. 如果有任何字段,则无法提供合理的GetHashCode()对象可以更改。通常,对象永远不会在集合依赖于GetHashCode()。所以成本实现GetHashCode()通常不值得,或者不值得可能。

  2. 如果有人将您的对象放入调用GetHashCode()并且您已经覆盖了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;
if (fooItem == null){return false;}
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 classthrow new Exception("Sorry I don't know what GetHashCode should do for this class");}}

下面使用反射在我看来是考虑公共属性的更好的选择,因为这样你就不必担心添加/删除属性(尽管不是那么常见的场景)。我发现这也表现得更好。(与使用对话式秒表的时间相比)。

    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;}

只是为了补充上述答案:

如果不重写Equals,则默认行为是比较对象的引用。这同样适用于哈希码-默认实现通常基于引用的内存地址。因为你确实覆盖了Equals,这意味着正确的行为是比较你在Equals上实现的任何内容,而不是引用,所以你应该对哈希码做同样的事情。

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

此外,除了在字典中使用对象时可能无法找到对象的问题(因为它是由一个哈希码插入的,当你查找它时,默认哈希码可能会不同,而且Equals()甚至不会被调用,就像Marc Gravell在他的回答中解释的那样,你还引入了违反字典或哈希集概念的行为,不应该允许相同的键-当你覆盖Equals时,你已经声明了这些对象本质上是相同的,所以你不希望它们都是数据结构上的不同键,因为它们应该有一个唯一的键。但是因为它们有不同的哈希码,“相同”的键将被插入为不同的键。

你应该始终保证如果两个对象相等,如Equals()所定义,它们应该返回相同的哈希代码。正如其他一些注释所述,理论上,如果该对象永远不会在HashSet或Dic的基于哈希的容器中使用,这不是强制性的。不过,我建议你始终遵循这条规则。原因很简单,对于某人来说,将集合从一种类型更改为另一种类型太容易了,目的是真正提高性能或只是以更好的方式传达代码语义学。

例如,假设我们在List中保留一些对象。一段时间后,有人实际上意识到HashSet是一个更好的选择,因为例如更好的搜索特征。这就是我们遇到麻烦的时候。List将在内部使用类型的默认相等比较器,在你的情况下意味着Equals,而HashSet使用GetHashCode()。如果两者的行为不同,你的程序也会不同。记住,此类问题不是最容易解决的问题。

我在博客文章中总结了这种行为和其他一些GetHashCode()陷阱,您可以在其中找到更多示例和解释。

.NET 4.7开始,覆盖GetHashCode()的首选方法如下所示。如果针对旧的。NET版本,请包含System. ValueTuple属性值包。

// C# 7.0+public override int GetHashCode() => (FooId, FooName).GetHashCode();

在性能方面,此方法将优于大多数复合哈希代码实现。ValueTuple类型struct,因此不会有任何垃圾,并且底层算法尽可能快。

c#9(. net 5或. net core 3.1)开始,您可能希望使用记录,因为默认情况下它使用基于价值的平等