C#中的GetHashCode指南

我阅读了基本的C#3.0和.NET 3.5书:

即使对象的数据发生变化,GetHashCode()在特定对象的生命周期内的返回值也应该是常数(相同的值)。 在许多情况下,您应该caching方法返回来执行此操作。

这是一个有效的指导方针吗?

我已经在.NET中尝试了几个内置types,他们不像这样。

答案大多是,这是一个有效的指导方针,但也许不是一个有效的规则。 这也不能说明整个故事。

需要指出的是,对于可变types,不能将哈希码作为可变数据的基础,因为两个相同的对象必须返回相同的哈希码,并且哈希码必须在对象的生命周期内有效。 如果哈希代码发生变化,最终会有一个对象在散列集合中丢失,因为它不再存在于正确的哈希列表中。

例如,对象A返回1的散列。所以它进入散列表的第一个仓库。 然后你改变对象A,使得它返回一个2的散列。当一个散列表去查找它时,它在bin 2中查找,并且找不到它 – 这个对象在bin 1中是孤立的。这就是为什么散列码必须不改变对象的生命周期 ,只是为什么编写GetHashCode实现的一个原因是在对接的痛苦。

更新
Eric Lippert发布​​了一个博客 ,提供有关GetHashCode优秀信息。

其他更新
我已经做了上面的几个更改:

  1. 我区分了准则和规则。
  2. 我为“终生的目标”而努力。

指南只是一个指导,而不是一个规则。 实际上, GetHashCode只有当事物期望对象遵循准则时(例如当它存储在散列表中时),才必须遵循这些准则。 如果你永远不打算在散列表中使用你的对象(或任何其他依赖于GetHashCode的规则),你的实现不需要遵循指导原则。

当你看到“对象的生命周期”,你应该阅读“对象需要与哈希表合作”或类似的时间。 像大多数事情一样, GetHashCode是关于何时破坏规则的知识。

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

但首先是:问题中引用的指导方针是错误的。

现在,他们有两个

首先为什么 :如果哈希码是以某种方式计算的,那么即使对象本身发生更改,它也不会在对象的生命周期内发生更改,而不会破坏等于约定。

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

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

想象两个包含名称的对象,其中名称在equals方法中使用:相同的名称 – >相同的东西。 创build实例A:Name = Joe创build实例B:Name = Peter

哈希码A和哈希码B很可能不会相同。 如果实例B的名称更改为Joe,现在会发生什么情况?

根据问题的指导,B的散列码不会改变。 这样的结果是:A.Equals(B)==> true但同时:A.GetHashCode()== B.GetHashCode()==> false。

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

第二个原因 :虽然当然是这样的,但是哈希码的变化可能会破坏散列表和使用哈希码的其他对象,反过来也是如此。 不改变散列码将在最坏的情况下得到散列表,其中所有不同的对象将具有相同的散列码,因此在相同的散列仓中 – 例如当对象用标准值初始化时发生。


现在到了这个地步,乍一看,似乎有一个矛盾 – 不pipe怎样,代码将会被破坏。 但是,没有问题来自改变或未改变的哈希码。

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

从MSDN的哈希表入口:

只要键对象在Hashtable中用作键,键对象就必须是不可变的。

这确实意味着:

任何创build散列值的对象都应该改变散列值,当对象发生变化时,当它在Hashtable(或任何其他使用Hash的对象)中使用时,绝对不能 – 允许对其本身进行任何更改。 。

首先,最简单的方法当然是devise仅用于散列表中的不可变对象,这将在需要时被创build为普通的可变对象的副本。 在不可变的对象内部,caching哈希码是可以的,因为它是不可变的。

第二,如何给对象一个“你现在被哈希了”的标志,确保所有的对象数据都是私有的,在所有可以改变对象数据的函数中检查标志,并且在不允许改变的情况下抛出exception数据(即设置标志)。 现在,当把对象放在任何散列区域时,一定要设置标志,并且 – 当标志不再需要的时候,不要标志。 为了便于使用,我build议在“GetHashCode”方法中自动设置标志 – 这种方式不能被遗忘。 而“ResetHashFlag”方法的显式调用将确保程序员将不得不思考,现在是否允许更改对象数据。

好的,应该说的是:有些情况下,可能有可变数据的对象,其中哈希码不变,当对象数据改变时,不违反equals&hashcode-contract。

然而,这并不需要,等式方法也不基于可变数据。 所以,如果我写一个对象,并创build一个GetHashCode方法,它只计算一次值并将其存储在对象中,以便在以后的调用中返回它,那么我必须:绝对必须创build一个Equals方法,它将使用存储的值进行比较,以便A.Equals(B)永远不会从false更改为true。 否则,合同将被打破。 这样做的结果通常是Equals方法没有任何意义 – 它不是原始的参考平等,但它也不是一个值的平等。 有时,这可能是有意的行为(即客户logging),但通常情况并非如此。

所以,只要让GetHashCode结果发生变化,当对象数据发生变化时,如果使用列表或对象的哈希内部对象的使用是有意(或者仅仅是可能的),那么使对象不可变或者创build一个只读标志来用于包含对象的哈希列表的生存期。

(顺便说一下:所有这些都不是C#和.NET特有的 – 它是所有哈希表实现的本质,或者更一般的任何索引列表,识别对象的数据不应该改变,而对象在列表中如果这个规则被破坏,会出现意想不到的不可预测的行为,在某个地方,可能会有一些列表实现,它们监视列表中的所有元素,并自动重新列表列表,但这些行为的performance最好还是可怕的。

来自MSDN

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

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

为了获得最佳性能,散列函数必须为所有input生成一个随机分布。

这意味着如果对象的值发生变化,哈希码应该改变。 例如,“Name”属性设置为“Tom”的“Person”类应该有一个哈希代码,如果将该名称更改为“Jerry”,则应该有一个不同的代码。 否则,汤姆==杰里,这可能不是你想要的。


编辑

另外从MSDN:

覆盖GetHashCode的派生类也必须重写Equals以保证两个被认为相等的对象具有相同的哈希码; 否则,Hashtabletypes可能无法正常工作。

从MSDN的哈希表入口 :

只要键对象在Hashtable中用作键,键对象就必须是不可变的。

我读到的方式是,可变对象应该返回不同的哈希码,因为它们的值会改变, 除非它们被devise用于散列表。

在System.Drawing.Point的例子中,该对象是可变的,并且当X或Y值改变时确实返回不同的哈希码。 这将使它成为一个可能的候选人在哈希表中使用。

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

一方面,MSDN指出一个对象的哈希代码不应该改变,而且是不变的。另一方面,MSDN也声明GetHashcode的返回值对于2个对象应该是相等的,如果这两个对象被认为是相等的话。

MSDN:

散列函数必须具有以下属性:

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

然后,这意味着所有的对象应该是不可变的,或者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中find的规则。 假设你有这个类的两个实例; instance1的Name属性设置为“Pol”,instance2的Name属性设置为“Piet”。 两个实例都返回一个不同的哈希码,而且它们也不相同。 现在,假设我将instance2的名称更改为“Pol”,然后根据我的Equals方法,两个实例应该是相等的,根据MSDN的规则之一,它们应该返回相同的哈希码。
但是,这是不能做到的,因为instance2的哈希码将会改变,而MSDN则说这是不允许的。

然后,如果你有一个实体,你可能可能实现哈希码,以便它使用该实体的“主标识符”,这可能是理想的代理键或不可变的属性。 如果你有一个值对象,你可以实现Hashcode,以便它使用该值对象的'属性'。 这些属性构成了价值对象的“定义”。 这当然是价值对象的本质; 你对它的身份不感兴趣,而是对它的价值感兴趣。
因此,价值对象应该是不变的。 (就像他们在.NET框架,string,date等…都是不可变的对象)。

另一件想到的事情是:
在“会话”期间(我真的不知道该怎么调用这个),“GetHashCode”返回一个常量值。 假设你打开你的应用程序,从DB(一个实体)加载一个对象的实例,并获得它的哈希码。 它会返回一定的数字。 closures应用程序,并加载相同的实体。 是否需要这次的散列码与第一次加载实体时的值相同? 恕我直言,不是。

这是个好build议。 布赖恩·佩平(Brian Pepin)在这件事上要说的是:

这已经让我不止一次:确保GetHashCode在一个实例的整个生命周期中总是返回相同的值。 请记住,散列码用于在大多数散列表实现中标识“桶”。 如果一个对象的“桶”发生变化,哈希表可能无法find你的对象。 这些可能是很难find的,所以第一次就做对了。

不直接回答你的问题,但是 – 如果你使用Resharper,不要忘了它有一个function,可以为你生成一个合理的GetHashCode实现(以及Equals方法)。 你当然可以指定在计算哈希码时将哪个类的成员考虑在内。

哈希码不会改变,但了解哈希码来自何处也很重要。

如果你的对象使用值语义,即对象的身份是由它的值(如string,颜色,所有结构)定义。 如果你的对象的身份独立于它的所有值,那么哈希码由其值的一个子集来标识。 例如,您的StackOverflow条目存储在某个数据库中。 如果您更改姓名或电子邮件地址,您的客户条目保持不变,但有些值已更改(最终通常由一些长客户ID标识)。

所以简而言之:

值types语义 – 哈希码由值定义参考types语义 – 哈希码由一些id定义

我build议你阅读埃里克·埃文斯(Eric Evans)的领域驱动devise(Domain Driven Design),如果这还没有意义,那么他将进入实体与价值types(这或多或less是我上面想要做的)。

看看Marc Brooks的博客文章:

VTO,RTO和GetHashCode() – 哦,我的!

然后检查后续的post(不能链接,因为我是新的,但有一个链接在initlal文章)进一步讨论,并涵盖了在初步实施中的一些小的弱点。

这是我创build一个GetHashCode()实现所需要知道的一切,他甚至还提供了他的方法以及其他一些实用工具的下载,简而言之,黄金。

查阅Eric Lippert的GetHashCode准则和规则