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


1444

鉴于以下课程

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


36
由于冲突,尤其是在使用字典时,必须实现equals和gethashcode,这一点很重要。如果两个对象返回相同的哈希码,则通过链接将它们插入到字典中。访问项目时使用equals方法。
DarthVader 2011年

Answers:


1319

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

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

  • 如果两个事物相等(Equals(...) == true),则它们必须返回相同的值GetHashCode()
  • 如果GetHashCode()是相等的,它是必要对他们是相同的; 这是一次碰撞,Equals将调用它来查看它是否是真正的相等性。

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

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


这里是当您弄错时将发生什么的一个演示。


49
请问您为什么要乘以这些因素?
LeandroLópez,2009年

22
实际上,我可能会丢掉其中一个。关键是要尽量减少冲突次数-使对象{1,0,0}的散列分别不同于{0,1,0}和{0,0,1}(如果您明白我的意思的话) ),
马克·格雷夫

13
我调整了数字以使其更清楚(并添加了一个种子)。某些代码使用不同的数字-例如,C#编译器(用于匿名类型)使用0x51ed270b的种子和-1521134295的因数。
马克·格雷韦尔

76
@LeandroLópez:通常将因子选择为质数,因为它使碰撞次数变小。
安德烈·雷内(AndreiRînea),2010年

29
“哦,为方便起见,在覆盖Equals和GethashCode时,您可能还考虑提供==和!=运算符。”:Microsoft不鼓励为非固定对象实现operator ==- msdn.microsoft.com/zh-cn/library/ ms173147.aspx- “在非不可变类型中重写运算符==不是一个好主意。”
antiduh 2012年

137

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

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


20
@vanja。我认为这与以下内容有关:如果将对象添加到字典中,然后更改对象的ID,则稍后获取时,您将使用其他哈希值来检索它,因此您永远不会从字典中获取它。
2010年

74
Microsoft的GetHashCode()函数文档既没有声明也没有暗示对象哈希在其生命周期内必须保持一致。实际上,它专门说明了一种可能的情况,其中可能不会:“只要没有修改确定对象的Equals方法返回值的对象状态,对象的GetHashCode方法就必须始终返回相同的哈希码。 ”。
PeterAllenWebb

37
“哈希码在对象的生存期内不应更改”-这是不正确的。
启示录

7
更好的说法是“在将对象用作集合键的期间,哈希码(或等于的散列值)应发生变化”,因此,如果将对象作为字典添加到字典中,则必须确保GetHashCode和Equals不会更改给定输入的输出,除非您从字典中删除该对象。
斯科特·张伯伦

11
@ScottChamberlain我认为您在评论中没有忘记,它应该是:“在对象被用作集合的键期间,哈希码(也不等于等于)不应该改变”。对?
Stan Prokop 2014年

57

通过覆盖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;
    }
}

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


7
这不总是返回零吗?可能应该将结果初始化为1!还需要更多的分号。
Sam Mackrill '02

16
您知道XOR运算符(^)的作用吗?
斯蒂芬·德鲁

1
正如我所说,这是R#为您编写的内容(至少是它在2008年所做的)。显然,该片段旨在由程序员以某种方式进行调整。至于缺少的分号...是的,当我从Visual Studio的区域选择中复制粘贴代码时,似乎将它们遗漏了。我还认为人们会同时解决这两个问题。
陷阱

3
@SamMackrill我已经添加了缺少的分号。
Matthew Murdoch

5
@SamMackrill不,它不会总是返回0 0 ^ a = a,所以0 ^ m_someVar1 = m_someVar1。他不妨将的初始值设置resultm_someVar1
米莉·史密斯

41

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

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

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


6
在子类将超类Equals方法作为其自身比较的一部分(即base.Equals(obj))引用子类的情况下,这种类型检查将失败-应该用作代替
sweetfa 2012年

@sweetfa:这取决于如何实现子类的Equals方法。它也可以调用base.Equals((BaseType)obj)),它将正常工作。
huha 2013年

2
不,它不会:msdn.microsoft.com/en-us/library/system.object.gettype.aspx。此外,方法的实现不应因调用方式而失败或成功。如果对象的运行时类型是某个基类的子类,则无论该基类的Equals()如何调用,该基类的Equals()obj的确应等于true this
木星

2
fooItemnull移至顶部,然后检查其是否为null,以防出现null或类型错误的情况。
IllidanS4希望莫妮卡回到

1
@ 40Alpha好吧,那么那obj as Foo将是无效的。
IllidanS4希望莫妮卡回到

35

怎么样:

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

假设性能不是问题:)


1
erm-但您要为基于int的方法返回字符串; _0
jim tollan 2011-02-17

32
不,他确实从String对象调用GetHashCode(),该对象返回一个int。
理查德·克莱顿

3
我不希望这会像我希望的那样快,不仅因为值类型涉及的拳击,而且还因为的性能string.Format。我见过的另一个令人讨厌的人是new { prop1, prop2, prop3 }.GetHashCode()。不能评论这两者之间哪一个比较慢。不要滥用工具。
nawfal 2013年

16
对于{ prop1="_X", prop2="Y", prop3="Z" }和将返回true { prop1="", prop2="X_Y", prop3="Z_" }。您可能不想要那样。
voetsjoeba 2014年

2
是的,您总是可以用不太常见的符号(例如•,▲,►,◄,☺,☻)替换下划线符号,并希望您的用户不要使用这些符号... :)
Ludmil Tinkov

13

我们有两个问题要解决。

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

5
从GetHashCode引发异常是违反对象协定的。定义一个GetHashCode函数使两个相等的对象返回相同的哈希码是没有困难的。return 24601;并且return 8675309;都是的有效实现GetHashCodeDictionary只有当项目数量少时,的性能才会令人满意;如果项目数量变大,则性能会变得很差,但是无论如何它都可以正常工作。
2013年

2
@supercat,如果对象中的标识字段可以更改,则不可能以明智的方式实现GetHashCode,因为哈希码必须永不更改。按照您说的话,可能导致某人不得不花很多时间来追踪性能问题,然后在大型系统上重新设计以消除字典的使用,这需要花费数周的时间。
伊恩·林罗斯

2
我曾经对我定义的所有需要​​Equals()的类都做过这样的事情,而且我完全确定我永远不会将该对象用作集合中的键。后来有一天,一个我使用像这样的对象作为DevExpress XtraGrid控件输入的程序崩溃了。事实证明,XtraGrid在我的背后,正在创建一个HashTable或基于我的对象的东西。我与DevExpress支持人员讨论了一个小问题。我说过,他们将组件的功能和可靠性基于晦涩的方法的未知客户实现并不明智。
RenniePet 2014年

DevExpress的人相当狡猾,基本上是说我必须是个笨蛋,才能在GetHashCode()方法中引发异常。我仍然认为他们应该找到一种做他们做的事情的替代方法-我记得Marc Gravell在另一个线程上,描述了他如何构建任意对象的字典而不依赖于GetHashCode()-无法回忆起他是如何做的虽然。
RenniePet 2014年

4
@RenniePet,最好由于抛出异常而暗恋,然后由于无效的实现而很难发现错误。
伊恩·林格罗斯

12

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


11

只是添加以上答案:

如果不覆盖Equals,则默认行为是比较对象的引用。哈希码也是如此-默认实现通常基于引用的内存地址。因为您确实覆盖了Equals,这意味着正确的行为是比较在Equals上实现的内容而不是引用,因此,您应该对哈希码执行相同的操作。

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

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


8

哈希代码用于基于哈希的集合,例如Dictionary,Hashtable,HashSet等。此代码的目的是通过将特定对象放入特定的组(存储桶)中,对它们进行快速预排序。当您需要从哈希集合中检索该对象时,此预排序将极大地帮助您找到该对象,因为代码必须仅在一个存储桶中而不是在其中包含的所有对象中搜索您的对象。哈希码的分布更好(唯一性更好),检索速度更快。在每个对象都有唯一的哈希码的理想情况下,找到它是一个O(1)操作。在大多数情况下,它接近O(1)。


7

它不一定重要;它取决于集合的大小和性能要求,以及类是否将在您可能不知道性能要求的库中使用。我经常知道我的集合大小不是很大,并且我的时间比通过创建完美的哈希码获得几微秒的性能更有价值。因此(为了摆脱编译器的烦人警告),我只需使用:

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

3
不赞成投票。这是完全错误的。甚至Microsoft在MSDN(msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx)中都声明,当对象状态以可能影响调用返回值的方式更改时,GetHashCode的值必须更改到Equals(),甚至在其示例中也显示了完全依赖可公开更改的值的GetHashCode实现。
塞巴斯蒂安·金格特

塞巴斯蒂安,我不同意:如果将对象添加到使用哈希码的集合中,则将其放入取决于哈希码的bin中。如果现在更改哈希码,则将在集合中找不到该对象,因为将搜索错误的bin。实际上,这就是我们的代码中发生的事情,这就是为什么我发现有必要指出这一点的原因。
ILoveFortran 2013年

2
塞巴斯蒂安(Sebastian),此外,我在链接(msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx)中看不到必须更改GetHashCode()的声明。相反,只要Equals为相同的参数返回相同的值,它就不得更改:“对象的GetHashCode方法必须一致地返回相同的哈希码,只要对确定返回值的对象状态没有任何修改即可。该语句的含义并不相反,如果Equals的返回值更改,则必须更改。
ILoveFortran

2
@Joao,您正在混淆合同的客户/消费者方与生产者/实施者。我说的是实现者的责任,该实现者重写GetHashCode()。您是在谈论消费者,即使用价值的消费者。
ILoveFortran 2013年

1
完全误解... :)事实是,除非对象的状态与对象的身份无关,否则哈希代码必须在对象的状态更改时更改。同样,您绝不应该将MUTABLE对象用作集合中的键。为此,请使用只读对象。GetHashCode,Equals ...以及其他一些我现在不记得其名称的方法都不要抛出。
darlove

0

您应始终保证,如果两个对象相等(如Equals()所定义),则它们应返回相同的哈希码。正如其他一些评论所指出的那样,如果从不将对象用于基于哈希的容器(如HashSet或Dictionary)中,则从理论上讲这不是强制性的。我建议您始终遵循此规则。原因仅仅是因为某人将一个集合从一种类型更改为另一种类型太容易了,其目的是实际提高性能或只是以更好的方式传达代码语义。

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

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


0

作为.NET 4.7首选的替代方法,GetHashCode()如下所示。如果目标是.NET的较早版本,请包括System.ValueTuple nuget包。

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

在性能方面,此方法将胜过大多数复合哈希代码实现。该ValueTuplestruct这样不会有任何垃圾,以及底层的算法是一样快,因为它得到。


-1

据我了解,原始的GetHashCode()返回对象的内存地址,因此,如果您想比较两个不同的对象,则必须重写它。

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


-6

在我看来,在下面使用反射似乎是考虑公共属性的一个更好的选择,因为您不必担心添加/删除属性(尽管不是很常见的情况)。我发现这也表现得更好。(与使用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;  
    }

12
预期GetHashCode()的实现非常轻巧。我不确定在数千个呼叫中使用StopWatch会引起明显的反射,但肯定会在数百万个呼叫中使用反射(想从列表中填充字典)。
bohdan_trotsenko 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.