给定下面的类,因为Equals代表Foo s表的行,所以我重写了Foo方法。覆盖GetHashCode的首选方法是哪种?

为什么覆盖GetHashCode的重要性为何?

评论

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

阅读此内容loganfranken.com/blog/692/overriding-equals-in-c-part-2

#1 楼

是的,将您的项目用作字典或HashSet<T>等中的键非常重要-因为使用该项目(在没有自定义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运算符。


此处是出现错误时发生情况的演示。 br />

评论


请问您为什么要乘以这些因素?

– LeandroLópez
09年1月16日在10:30

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

– Marc Gravell♦
2009年1月16日13:45

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

– Marc Gravell♦
09年1月16日在13:49

@LeandroLópez:通常将因数选择为质数,因为它使碰撞次数减少。

– AndreiRînea
2010-10-22 23:25

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

–antiduh
2012年5月9日20:04



#2 楼

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

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

评论


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

– ANeves认为SE是邪恶的
2010-12-21在16:18

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

– PeterAllenWebb
2012年10月4日18:44



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

–启示录
13年3月29日在11:23

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

–斯科特·张伯伦
13年8月11日在5:56

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

– Stan Prokop
2014年4月27日在19:33

#3 楼

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


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

评论


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

–山姆·麦克里尔(Sam Mackrill)
2012年2月21日14:14

您知道XOR运算符(^)的作用吗?

–斯蒂芬·德鲁(Stephen Drew)
2012年4月9日在11:19

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

–陷阱
2012年4月13日在12:11

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

–马修·默多克(Matthew Murdoch)
13年4月3日在20:14

@SamMackrill不,它不会总是返回0。0 ^ a = a,所以0 ^ m_someVar1 = m_someVar1。他最好将result的初始值设置为m_someVar1。

–米莉·史密斯(Millie Smith)
17年2月16日在0:22

#4 楼

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

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

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

    return fooItem.FooId == this.FooId;
}


原因如下:Equalsnull比较时必须返回false。另请参见http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

评论


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

–sweetfa
2012年8月21日,下午2:55

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

–呵呵
13年8月27日在10:03

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

–木星
2013年9月24日20:57



将fooItem移至顶部,然后检查其是否为null,以防出现null或错误类型的情况。

– IllidanS4支持Monica
17年2月6日在11:06

@ 40Alpha好吧,是的,然后obj作为Foo将无效。

– IllidanS4支持Monica
18年2月15日在19:09

#5 楼

怎么样:

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



假设性能不是问题:)


评论


erm-但是您要为基于int的方法返回字符串; _0

– Jim Tollan
2011-2-17在12:23

不,他确实从String对象调用GetHashCode(),该对象返回一个int。

–理查德·克莱顿(Richard Clayton)
2011年4月25日在12:26

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

– nawfal
2013年12月15日10:33



对于{prop1 =“ _ X”,prop2 =“ Y”,prop3 =“ Z”}和{prop1 =“”,prop2 =“ X_Y”,prop3 =“ Z_”},这将返回true。您可能不想要那样。

–voetsjoeba
2014年1月2日,19:43



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

–卢德米尔·廷科夫(Ludmil Tinkov)
2014年9月3日23:13

#6 楼

我们有两个问题要解决。

如果可以更改
对象中的任何字段,则无法提供合理的GetHashCode()。同样,在依赖于GetHashCode()
集合中,绝不会使用任何对象。因此,
实施GetHashCode()的成本通常是不值得的,或者是不可行的。
如果有人将您的对象放入调用
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");
    }
}


评论


从GetHashCode引发异常是违反对象协定的。毫不费力地定义一个GetHashCode函数,以使相等的任何两个对象返回相同的哈希码。返回24601;并返回8675309;都将是GetHashCode的有效实现。仅当项数少时,词典的性能才是不错的,而如果项数变大,字典的性能将变得很差,但是无论如何它都能正常工作。

–超级猫
2013年12月19日上午7:11

@supercat,如果对象中的标识字段可以更改,则无法以明智的方式实现GetHashCode,因为哈希码必须永不更改。按照您说的话,可能导致某人不得不花很多时间来跟踪性能问题,然后在大型系统上重新设计要花几周的时间才能删除字典。

–伊恩·林格罗斯(Ian Ringrose)
2013年12月19日上午9:41

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

–RenniePet
14年6月29日在4:27

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

–RenniePet
14年6月29日在4:32

@RenniePet,最好由于抛出异常而暗恋,然后由于无效的实现而很难发现错误。

–伊恩·林格罗斯(Ian Ringrose)
2014年6月29日19:53

#7 楼

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

#8 楼

只是为了补充上面的答案:

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

类的客户端将期望哈希码具有与equals方法类似的逻辑,例如,使用IEqualityComparer的linq方法首先比较哈希码,并且只有在它们相等时才比较等于()方法的执行成本可能更高,如果我们未实现哈希码,则相等的对象可能会具有不同的哈希码(因为它们具有不同的内存地址),并且会被错误地确定为不相等(Equals()甚至不会

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

#9 楼

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

#10 楼

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

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


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

当您确实需要表现时,当然这里其他人提到的所有问题都不适用。最重要的是-否则从哈希集或字典中检索项目时,您将得到错误的结果:哈希码不得随对象的生存时间而变化(更准确地说,在需要哈希码的时间段内,例如字典中的键):例如,以下错误,因为Value是公共的,因此可以在实例的生存期内在类的外部进行更改,因此您不得将其用作哈希代码的基础: br />

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



评论


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

– Sebastian P.R. Gingter
13年5月22日在9:27



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

– ILoveFortran
13年5月24日在12:19

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

– ILoveFortran
13年5月24日在12:45

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

– ILoveFortran
13年7月8日在15:39

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

– darlove
17-10-11在11:28

#11 楼

.NET 4.7开始,覆盖GetHashCode()的首选方法如下所示。如果定位到较早的.NET版本,请包括System.ValueTuple nuget包。

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


在就性能而言,此方法将胜过大多数复合哈希代码实现。 ValueTuple是一个struct,因此不会有任何垃圾,而且基础算法的运行速度也很快。

#12 楼

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

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

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

#13 楼

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

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

#14 楼

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


评论


预期GetHashCode()的实现非常轻巧。我不确定秒表在成千上万的呼叫中使用反射是否很明显,但在数以百万计的呼叫中肯定使用反射(想从列表中填充字典)。

– bohdan_trotsenko
2014年12月12日上午9:45