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
优秀信息。
其他更新
我已经做了上面的几个更改:
- 我区分了准则和规则。
- 我为“终生的目标”而努力。
指南只是一个指导,而不是一个规则。 实际上, 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准则和规则
- String.Join忽略空string的方法?
- 如何编组multidimensional array
- HttpClient.GetAsync(…)在使用await / async时永远不会返回
- 使用返回随机结果的函数进行unit testing
- 如何以编程方式在C#中安装Windows服务?
- 仅entity framework代码错误:自创build数据库以来,支持上下文的模型已更改
- parsing器错误消息:无法加载types'TestMvcApplication.MvcApplication'
- 使用Html.TextBoxFor时,可以将文本框设置为只读吗?
- 使用.NET 4.5 HttpClient的代理