C#中的GetHashCode准则


136

我在Essential C#3.0和.NET 3.5书中读到:

即使特定对象的数据发生更改,GetHashCode()在特定对象的整个生命周期内的返回值也应保持恒定(相同的值)。在许多情况下,您应该缓存方法return以强制执行此操作。

这是有效的指南吗?

我在.NET中尝试了几种内置类型,但它们的行为不像这样。


如果可能,您可能要考虑更改接受的答案。
Giffyguy 2015年

Answers:


93

答案主要是,这是有效的准则,但可能不是有效的规则。它也不能说明全部故事。

需要说明的是,对于可变类型,您不能将哈希码基于可变数据,因为两个相等的对象必须返回相同的哈希码,并且哈希码在对象的生存期内必须有效。如果哈希码发生变化,最终将导致一个对象丢失在哈希集合中,因为该对象不再存在于正确的哈希箱中。

例如,对象A返回哈希1。因此,它进入哈希表的bin 1。然后,更改对象A,使其返回2的哈希值。当哈希表查找它时,它将在bin 2中查找并且找不到它-该对象在bin 1中是孤立的。这就是为什么哈希码必须在对象的整个生命周期中都不会改变,而编写GetHashCode实现的原因之一就是痛苦。

更新
Eric Lippert发布​​了一个博客该博客提供了有关的出色信息GetHashCode

其他更新
我在上面做了几处更改:

  1. 我对准则和规则进行了区分。
  2. 我打通了“对象的生命周期”。

指南只是一个指南,而不是规则。实际上,GetHashCode只有在事物期望对象遵循准则时(例如将其存储在哈希表中时),才必须遵循这些准则。如果您从不打算在哈希表中使用对象(或其他依赖于以下规则的对象)GetHashCode),则您的实现无需遵循准则。

当看到“对象的生存期”时,应阅读“对象需要与哈希表协作的时间”或类似内容。像大多数事情一样,GetHashCode是关于知道何时违反规则的事情。


1
您如何确定可变类型之间的相等性?
乔恩B

9
您不应该使用GetHashCode来确定相等性。
JSBձոգչ09年

4
@JS Bangs-来自MSDN:重写GetHashCode的派生类也必须重写Equals,以确保被认为相等的两个对象具有相同的哈希码;否则,哈希表类型可能无法正常工作。
Jon B

3
@琼·芬格:两件事。首先,即使在每个实现中,微软都没有正确使用GetHashCode。其次,值类型通常是不可变的,每个值都是一个新实例,而不是对现有实例的修改。
Jeff Yates

17
由于a.Equals(b)必须表示a.GetHashCode()== b.GetHashCode(),因此,如果更改用于相等性比较的数据,则哈希码通常必须更改。我会说问题不是GetHashCode基于可变数据。问题是使用可变对象作为哈希表键(并实际上对其进行了突变)。我错了吗?
尼古拉斯

120

已经有很长的时间了,但是我认为仍然有必要对这个问题给出正确的答案,包括对原因和方式的解释。到目前为止,最好的答案是详尽地援引MSDN的答案-不要试图制定自己的规则,MS家伙知道他们在做什么。

但首先要注意的是:问题中引用的准则是错误的。

现在的原因-其中有两个

首先为什么:如果哈希码是以某种方式计算的,则即使对象本身发生更改,哈希码在对象的生命周期中也不会改变,否则它将破坏equals-contract。

请记住:“如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值。但是,如果两个对象比较不相等,则两个对象的GetHashCode方法不必返回不同的值。”

第二句话经常被误解为“唯一的规则是,在对象创建时,相等对象的哈希码必须相等”。真的不知道为什么,但这也是这里大多数答案的实质。

考虑两个包含名称的对象,其中在equals方法中使用该名称:同名->同一个东西。创建实例A:名称= Joe创建实例B:名称= Peter

哈希码A和哈希码B很可能会不同。当实例B的名称更改为Joe时,会发生什么?

根据问题的指导原则,B的哈希码不会更改。结果是:A.Equals(B)==> true,但同时:A.GetHashCode()== B.GetHashCode()==> false。

但是equals&hashcode-contract明确禁止这种行为。

第二个原因:尽管当然是正确的,但哈希码中的更改可能会破坏使用哈希码的哈希表和其他对象,反之亦然。在最坏的情况下,不更改哈希码将得到哈希表,在哈希表中,所有许多不同的对象将具有相同的哈希码,因此位于同一哈希箱中-例如,在使用标准值初始化对象时发生。


现在开始讨论吧,乍一看,似乎存在矛盾-无论哪种方式,代码都会中断。但是,这两个问题都不会来自更改或未更改的哈希码。

在MSDN中很好地描述了问题的根源:

从MSDN的哈希表条目中:

只要键对象在哈希表中用作键,它们就必须是不可变的。

这确实意味着:

创建哈希值的任何对象都应在该对象更改时更改该哈希值,但当在哈希表(或其他任何使用哈希的对象)中使用它时,绝对不能(绝对不允许)对其自身进行任何更改。

首先,最简单的方法当然是设计仅用于哈希表的不可变对象,该哈希表将在需要时创建为普通可变对象的副本。在不可变对象内部,可以很好地缓存哈希码,因为它是不可变的。

第二个方法或给对象一个“您现在被哈希”标志,确保所有对象数据都是私有的,检查所有可以更改对象数据的函数中的标志,如果不允许更改,则抛出异常数据(即设置了标志) )。现在,当您将对象放置在任何哈希区域中时,请确保设置该标志,并在不再需要该标志时取消设置该标志。为了易于使用,我建议您在“ GetHashCode”方法中自动设置该标志-这样就不会忘记它。而且,对“ ResetHashFlag”方法的显式调用将确保程序员现在必须考虑是否允许更改对象数据。

好的,还应该说一句:在某些情况下,当对象数据发生更改而没有违反equals&hashcode-contract时,可能具有可变数据的对象,但哈希码仍保持不变。

但是,这确实要求equals-method也不基于可变数据。因此,如果我编写一个对象,并创建一个只计算一次值并将其存储在对象中以在以后的调用中返回它的GetHashCode方法,那么我必须再次:绝对必须创建一个Equals方法,该方法将使用存储用于比较的值,以便A.Equals(B)也永远不会从false更改为true。否则,合同将被破坏。这样的结果通常是Equals方法没有任何意义-它不是原始引用等于,但也不是一个值也等于。有时,这可能是预期的行为(即客户记录),但通常并非如此。

因此,只要使GetHashCode结果发生变化,当对象数据发生变化时,并且如果打算(或仅可能)在哈希内部使用列表或对象使用对象,则使该对象不可变或创建一个只读标志以用于包含对象的哈希列表的生存期。

(顺便说一句:所有这些都不是C#oder .NET特有的-它是所有哈希表实现的本质,或更笼统地说是任何索引列表的本质,当对象在列表中时,对象的标识数据不应更改。如果违反此规则,则会发生意外的行为和不可预测的行为。某个地方可能存在列表实现,这些实现确实监视列表中的所有元素并自动为列表重新编制索引,但这些性能的充其量肯定是可怕的。)


23
+1进行详细说明(如果可以的话,会提供更多信息)
Oliver

5
+1,因为冗长的解释,绝对是更好的答案!:)

9

MSDN

如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值。但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值。

只要没有修改确定对象的Equals方法返回值的对象状态,对象的GetHashCode方法就必须始终返回相同的哈希码。请注意,这仅适用于当前执行的应用程序,并且如果再次运行该应用程序,则可以返回不同的哈希码。

为了获得最佳性能,哈希函数必须为所有输入生成随机分布。

这意味着,如果对象的值更改,则哈希码也应更改。例如,将“名称”属性设置为“汤姆”的“人”类应具有一个哈希码,如果将名称更改为“杰瑞”,则应具有不同的代码。否则,Tom == Jerry,这可能不是您想要的。


编辑

同样从MSDN:

重写GetHashCode的派生类还必须重写Equals,以确保被认为相等的两个对象具有相同的哈希码。否则,哈希表类型可能无法正常工作。

MSDN的哈希表条目中

只要键对象在哈希表中用作键,它们就必须是不可变的。

我读这本书的方式是,可变对象应该随着其值的改变而返回不同的哈希码,除非它们被设计用于哈希表。

在System.Drawing.Point的示例中,该对象是可变的,并且当X或Y值更改时,确实会返回不同的哈希码。这将使其成为在哈希表中按原样使用的较差的候选对象。


GetHashCode()设计用于哈希表,这是此函数的唯一要点。
skolima 2010年

@skolima-MSDN文档与此不一致。可变对象可以实现GetHashCode(),并且当对象的值更改时应返回不同的值。哈希表必须使用不可变键。因此,您可以将GetHashCode()用于哈希表以外的其他内容。
Jon B 2010年

9

我认为有关GetHashcode的文档有些混乱。

一方面,MSDN声明对象的哈希码永远不应该更改,并且是恒定的;另一方面,MSDN还声明如果两个对象被认为相等,则GetHashcode的返回值应等于2个对象。

MSDN:

哈希函数必须具有以下属性:

  • 如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值。但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值。
  • 只要没有修改确定对象的Equals方法返回值的对象状态,对象的GetHashCode方法就必须始终返回相同的哈希码。请注意,这仅适用于当前执行的应用程序,并且如果再次运行该应用程序,则可以返回不同的哈希码。
  • 为了获得最佳性能,哈希函数必须为所有输入生成随机分布。

然后,这意味着所有对象都应该是不可变的,或者GetHashcode方法应基于对象的不可变属性。假设您有此类(天真的实现):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

此实现已违反MSDN中可以找到的规则。假设您有2个此类的实例;instance1的Name属性设置为'Pol',instance2的Name属性设置为'Piet'。两个实例都返回不同的哈希码,并且它们也不相等。现在,假设我将instance2的名称更改为'Pol',然后根据我的Equals方法,两个实例应该相等,并且根据MSDN的规则之一,它们应该返回相同的哈希码。
但是,这无法完成,因为instance2的哈希码将更改,并且MSDN声明不允许这样做。

然后,如果您有一个实体,则可以实现哈希码,以便它使用该实体的“主标识符”,这在理想情况下可能是代理键或不可变属性。如果您有一个值对象,则可以实现哈希码,以便它使用该值对象的“属性”。这些属性构成了值对象的“定义”。当然,这是价值对象的本质。您对它的身份不感兴趣,但对它的价值感兴趣。
因此,值对象应该是不变的。(就像它们在.NET Framework中一样,字符串,日期等都是不可变的对象)。

想到的另一件事是:
在哪个“会话”(我真的不知道该如何称呼)期间,“ GetHashCode”应返回恒定值。假设您打开应用程序,从数据库(实体)中加载对象的实例,然后获取其哈希码。它将返回一定的数字。关闭应用程序,并加载相同的实体。是否要求这次的哈希码具有与第一次加载实体时相同的值?恕我直言,不是。


1
您的示例就是为什么Jeff Yates说您不能将哈希码基于可变数据。如果哈希码基于该对象的可变值,则不能将可变对象粘贴在Dictionary中并期望它能正常工作。
Ogre Psalm33,2009年

3
我看不到在哪里违反了MSDN规则?该规则明确规定:对象的GetHashCode方法必须一致地返回相同的哈希码,只要对对象状态没有任何修改即可确定该对象的Equals方法的返回值。这意味着当您将instance2的名称更改为Pol时,允许更改instance2的哈希码
chikak

8

这是个好建议。这是Brian Pepin在此问题上要说的:

这使我不止一次被绊倒:确保GetHashCode在实例的生存期内始终返回相同的值。请记住,在大多数哈希表实现中,哈希码用于标识“存储桶”。如果对象的“存储桶”发生更改,则哈希表可能无法找到您的对象。这些可能是很难找到的错误,因此请在第一时间正确解决。


我没有对此投反对票,但我猜想其他人也投了赞成票,因为它的报价无法涵盖整个问题。假装的字符串是可变的,但没有更改哈希码。您创建“ bob”,将其用作哈希表中的键,然后将其值更改为“ phil”。接下来创建一个新字符串“ phil”。如果您随后查找带有键“ phil”的哈希表条目,则将找不到最初放入的项。如果有人搜索“ bob”,它将被找到,但是您会得到一个不再正确的值。要么不要使用可变的钥匙,要么要注意危险。
埃里克·塔特尔曼

@EricTuttleman:是我写的一个框架的规则,我会规定,对于任何对对象XY,一次X.Equals(Y)Y.Equals(X)已被调用,所有未来的调用应产生相同的结果。如果要使用其他相等性定义,请使用EqualityComparer<T>
2013年

5

不是直接回答您的问题,而是-如果您使用Resharper,请不要忘记它具有为您生成合理的GetHashCode实现(以及Equals方法)的功能。当然,您可以指定在计算哈希码时将考虑类的哪些成员。


谢谢,实际上我从未使用过Resharper,但我一直看到它经常被提及,因此我应该尝试一下。
Joan Venge

+1 Resharper(如果有的话)会生成一个不错的GetHashCode实现。
ΩmegaMan

5

查看来自Marc Brooks的博客文章:

VTO,RTO和GetHashCode()-哦,天哪!

然后查看后续帖子(由于我是新手,所以无法链接,但是初始文章中有链接),该文章进一步讨论并涵盖了初始实现中的一些小弱点。

这就是创建GetHashCode()实现所需的一切,他甚至提供了下载方法以及其他实用程序的简短信息。


4

哈希码永远不会改变,但是了解哈希码的来源也很重要。

如果您的对象使用值语义,即对象的身份由其值定义(如String,Color,所有结构)。如果对象的身份独立于其所有值,则哈希码由其值的子集标识。例如,您的StackOverflow条目存储在某个地方的数据库中。如果更改名称或电子邮件,尽管某些值已更改(最终通常由一些较长的客户ID#标识),但您的客户条目保持不变。

简而言之:

值类型语义-哈希码由值定义引用类型语义-哈希码由某些ID定义

我建议您阅读埃里克·埃文斯(Eric Evans)的《域驱动设计》(Domain Driven Design)。


这不是真的正确。对于特定实例,哈希码必须保持不变。在值类型的情况下,通常每个值都是唯一的实例,因此,哈希值似乎发生了变化,但实际上它是一个新实例。
Jeff Yates

没错,值类型是不可变的,因此它们无法更改。接得好。
DavidN

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.