当重写Equals方法时,为什么重写GetHashCode方法很重要?

1656

给定以下类:

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代表了Foo表中的一行。那么覆盖GetHashCode的首选方法是什么?

为什么重写GetHashCode很重要?


53
在使用字典时,实现equals和gethashcode非常重要,因为会发生碰撞。如果两个对象返回相同的哈希码,则它们将用链接方式插入字典中。在访问该项时,将使用equals方法。 - DarthVader
6
阅读此文章 https://www.loganfranken.com/blog/692/overriding-equals-in-c-part-2/ - Miguel Domingues
4
使用Visual Studio,我们可以基于类属性生成Equals()和GetHashCode()方法。请参考此链接:https://learn.microsoft.com/zh-cn/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019 - Danushka Chathuranga
15个回答

1480

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

GetHashCode()方法应反映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方法时,您可能还应考虑提供==!=运算符。


当您犯错时会发生什么,这里有一个演示。


58
我可以问一下为什么你要用这些因子进行乘法运算吗? - Leandro López
29
实际上,我可能可以少用一个;重点是尽量减少冲突的数量——因此,一个对象{1,0,0}与{0,1,0}和{0,0,1}具有不同的哈希(如果你明白我的意思的话)。 - Marc Gravell
17
我调整了数字,以使它更清晰(并添加了一个种子)。一些代码使用不同的数字 - 例如 C# 编译器(用于匿名类型)使用种子为 0x51ed270b 和因子为 -1521134295。 - Marc Gravell
86
通常选择质数作为因子,因为这会使冲突数量更少。 - Andrei Rînea
35
为了方便起见,当重写Equals和GetHashCode时,您可能还需要考虑提供==和!=运算符。微软不鼓励在非不变类型中实现operator==。请参阅http://msdn.microsoft.com/en-us/library/ms173147.aspx。 - antiduh
显示剩余20条评论

153

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

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


23
我认为这与以下有关:如果您将对象添加到字典中,然后更改对象的ID,那么在稍后检索时,您将使用不同的哈希来检索它,因此您将无法从字典中获取该对象。 - ANeves
88
微软文档中的GetHashCode()方法并没有说明或暗示对象哈希必须在其生命周期内保持一致。实际上,它特别解释了一个允许的情况,即可能不一致:"对于一个对象的GetHashCode方法,在对象状态没有发生修改以决定返回值的Equals方法时,它必须始终返回相同的哈希码。" - PeterAllenWebb
42
“哈希码在对象的生命周期中不应更改” - 这并不正确。 - apocalypse
12
更好的说法是,“哈希码(或Equals方法的计算结果)在对象被用作集合键期间不应更改”。因此,如果您将对象添加到字典中作为键,必须确保对于给定的输入,GetHashCode和Equals方法不会在从字典中移除对象之前改变它们的输出。 - Scott Chamberlain
14
@ScottChamberlain,我认为你在评论中忘记了“NOT”,应该是:“哈希码(或等式的计算结果)在对象用作集合键期间不应更改”。对吗? - Stan Prokop
显示剩余6条评论

73

通过重写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;
    }
}

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


8
这段话的意思是:“这段代码是否会一直返回0?可能需要将结果初始化为1!还需要加入更多的分号。” - Sam Mackrill
20
你知道异或运算符(^)是做什么的吗? - Stephen Drew
3
@SamMackrill,我已经添加了缺失的分号。 - Matthew Murdoch
8
дёҚдјҡжҖ»жҳҜиҝ”еӣһ0гҖӮ0 ^ a = aпјҢеӣ жӯӨ 0 ^ m_someVar1 = m_someVar1гҖӮд»–еҸҜд»Ҙе°ҶеҸҳйҮҸresultзҡ„еҲқе§ӢеҖји®ҫзҪ®дёәm_someVar1гҖӮ - Millie Smith

48

.NET 4.7 开始,优先使用下面的方法重写 GetHashCode()。如果需要支持旧版本的 .NET,请安装 System.ValueTuple nuget 包。

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

就性能而言,此方法将胜过大多数复合哈希码实现。 ValueTuple 是一个struct,因此不会产生任何垃圾,底层算法速度达到最快。


1
或者,可以使用HashCode.Combine(FooId, FooName)ValueTuple<...>.GetHashCode()在底层使用了HashCode.Combine(参考链接:https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/ValueTuple.cs,4ad64bceec148f3c,references)。 - undefined

44
请在覆盖 Equals() 方法时不要忘记检查 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

8
当子类在其自己的比较中引用基类Equals方法(即base.Equals(obj))时,该类型检查将失败。应该使用as代替。 - sweetfa
1
@sweetfa:这取决于子类的Equals方法如何实现。它也可以调用base.Equals((BaseType)obj)),这样就可以正常工作了。 - huha
3
不会的:http://msdn.microsoft.com/en-us/library/system.object.gettype.aspx。而且,一个方法的实现不应该根据被调用的方式成功或失败。如果对象的运行时类型是某个基类的子类,则基类的Equals()应该返回true,如果`obj`确实等于`this`,则不管基类的Equals()如何调用。 - Jupiter
3
fooItem移到顶部,然后检查它是否为null,在处理null或错误类型的情况下性能更好。 - IS4
@IllidanS4,除非你正在使用值类型。但是那么你为什么要重写值类型的Equals呢? - S1r-Lanzelot
1
@40Alpha 嗯,是的,那么 obj as Foo 就是无效的。 - IS4

44

如何考虑这个:

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

假设性能不是问题 :)


2
但是你正在为一个基于整数的方法返回一个字符串 ;_0 - jim tollan
36
不,他确实从 String 对象调用了 GetHashCode() 方法,该方法返回一个整数。 - Richard Clayton
5
我不指望这个过程会像我期望的那样快,不仅是因为要涉及值类型的操作,还有string.Format的性能问题。另一个我见过的极客做法是使用new { prop1, prop2, prop3 }.GetHashCode()。但我无法确定这两种方式哪一个更慢。不要滥用工具。 - nawfal
22
对于 { prop1="_X", prop2="Y", prop3="Z" }{ prop1="", prop2="X_Y", prop3="Z_" },这将返回true。你可能不希望出现这种情况。 - voetsjoeba
3
没错,你总是可以用一些不太常见的字符(例如•、▲、►、◄、☺、☻)来替换下划线符号,并希望你的用户不会使用这些符号... :) - Ludmil Tinkov
显示剩余2条评论

19

仅为以上回答添加一点:

如果您不覆盖Equals,则默认行为是比较对象的引用。哈希码同样适用-默认实现通常基于引用的内存地址。由于您已覆盖了Equals,这意味着正确的行为是比较您在Equals中实现的内容,而不是引用,因此您应该对哈希码采取相同的做法。

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

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


18

这是因为框架要求相同的两个对象必须具有相同的哈希码。如果您覆盖equals方法以执行两个对象的特殊比较,并且该方法认为这两个对象是相同的,则这两个对象的哈希码也必须相同。(字典和哈希表依赖于此原则)。


好的,现在我明白了。如果两个不同实例的哈希码不同但它们应该被视为逻辑上相等,那么你不能可靠地在字典或哈希表中使用它们。这是一个抽象的要求,但现在我理解了。 - Suncat2000

16
我们需要处理两个问题。 1. 如果对象中的任何字段都可以更改,那么就无法提供一个合理的 GetHashCode()。而且通常一个对象永远不会被用于依赖于 GetHashCode() 的集合中,因此实现 GetHashCode() 的成本往往是不值得的,或者不可能实现。 2. 如果有人将你的对象放入调用 GetHashCode() 的集合中,并且你重写了 Equals() 但没有使 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;

        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 class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}

8
从 GetHashCode 抛出异常违反了对象协定。定义一个GetHashCode函数,使得任何两个相等的对象返回相同的哈希码没有困难; return 24601;return 8675309; 都是 GetHashCode 的有效实现。当项数较少时,Dictionary 的性能只会是合理的,并且如果项数变大,性能将变得非常糟糕,但在任何情况下都可以正常工作。 - supercat
8
如果对象中的标识字段可以更改,则无法以明智的方式实现GetHashCode,因为哈希码绝不能更改。按照您所说的做法可能会导致某人花费很多天追踪性能问题,然后在大型系统上花费数周重新设计以删除字典的使用。 - Ian Ringrose
4
我曾经为我定义的所有需要Equals()方法的类做过类似的事情,而且我完全确定我永远不会将该对象用作集合中的键。然后有一天,一个使用了这种对象作为DevExpress XtraGrid控件输入的程序崩溃了。原来,XtraGrid在背后基于我的对象创建了一个哈希表或类似的东西。我跟DevExpress支持人员就此发生了轻微争执。我说他们的组件功能和可靠性建立在一个未知的客户实现的模糊方法上是不明智的。 - RenniePet
3
DevExpress的人态度颇为讽刺,基本上是在暗示我在GetHashCode()方法中抛出异常的行为很蠢。尽管如此,我仍认为他们应该找到另一种方法来完成他们正在做的事情。我记得Marc Gravell在不同的线程上描述过,他如何构建一个任意对象的字典,而不依赖于GetHashCode()方法,但我不记得他是怎么做的了。 - RenniePet
6
@RenniePet,与其由于无效实现而遇到很难找的错误,不如因抛出异常而暗恋更好。 - Ian Ringrose
显示剩余7条评论

11

哈希码(Hash code)用于基于哈希的集合,如字典(Dictionary)、哈希表(Hashtable)、哈希集合(HashSet)等。它的作用是通过将特定对象放入特定组(桶)中非常快速地对其进行预排序。这种预排序在需要从哈希集合中检索对象时极大地帮助查找它,因为代码只需要在一个桶中搜索您的对象,而不是在包含的所有对象中搜索。哈希码分布越好(唯一性越好),检索速度就越快。在每个对象都具有唯一哈希码的理想情况下,寻找它是一个O(1)操作。在大多数情况下,它接近O(1)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,