我在Essential C#3.0和.NET 3.5书中读到:
GetHashCode()在特定对象的生命周期内的返回应该是常量(相同的值),即使对象的数据发生了变化.在许多情况下,您应该缓存方法返回以强制执行此操作.
这是一个有效的指导方针吗?
我在.NET中尝试了几种内置类型,但它们的行为并不像这样.
已经很长一段时间了,但我认为仍然有必要对这个问题给出正确的答案,包括对这些问题的解释.到目前为止,最好的答案是引用MSDN的人 - 不要试图制定自己的规则,MS人员知道他们在做什么.
但首先要做的是:问题中引用的指南是错误的.
现在是为什么 - 有两个
第一个原因:如果哈希码是以某种方式计算的,那么它在对象的生命周期内不会发生变化,即使对象本身发生变化,也不会破坏等于契约.
请记住:"如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值.但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值."
第二句经常被误解为"唯一的规则是,在对象创建时,相等对象的哈希码必须相等".不知道为什么,但这也是大多数答案的本质.
想想两个包含名称的对象,其名称在equals方法中使用:相同名称 - >相同的东西.创建实例A:Name = Joe创建实例B:Name = Peter
Hashcode A和Hashcode B很可能不一样.当实例B的名称更改为Joe时,现在会发生什么?
根据问题的指导原则,B的哈希码不会改变.结果如下:A.Equals(B)==> true但同时:A.GetHashCode()== B.GetHashCode()==> false.
但正是这种行为被equals&hashcode-contract明确禁止.
第二个原因:虽然它当然是 - 但是,哈希码中的更改可能会使用哈希码破坏散列列表和其他对象,反之亦然.在最坏的情况下,不更改哈希码将获得散列列表,其中所有大量不同的对象将具有相同的哈希码,因此在相同的哈希箱中 - 例如,当使用标准值初始化对象时发生.
现在来看看嗯,乍一看,似乎有一个矛盾 - 无论哪种方式,代码都会破裂.但这两个问题都不是来自更改或未更改的哈希码.
问题的根源在MSDN中有详细描述:
从MSDN的哈希表条目:
密钥对象必须是不可变的,只要它们在Hashtable中用作密钥即可.
这意味着:
当对象发生变化时,任何创建哈希值的对象都应该更改哈希值,但是当它在Hashtable(或任何其他使用Hash的对象)中使用时,它不能 - 绝对不能 - 允许对自身进行任何更改.
首先,最简单的方法当然是设计仅用于哈希表的不可变对象,这些对象将在需要时创建为普通对象的可复制对象.在不可变对象内部,缓存哈希码显然是可以的,因为它是不可变的.
第二个如何或者给对象一个"你现在被哈希"-flag,确保所有对象数据都是私有的,检查所有可以改变对象数据的函数中的标志,如果不允许更改则抛出异常数据(即标志设置) ).现在,当您将对象放在任何散列区域时,请确保设置标志,并且 - 以及 - 在不再需要时取消设置标志.为了便于使用,我建议在"GetHashCode"方法中自动设置标志 - 这样就不会忘记.并且显式调用"ResetHashFlag"方法将确保程序员必须思考,现在是否允许更改对象数据.
好吧,还应该说些什么:有些情况下,当对象数据发生变化时,可以使对象具有可变数据,其中哈希码仍未改变,而不违反equals&hashcode-contract.
然而,这确实需要equals方法也不基于可变数据.所以,如果我编写一个对象,并创建一个GetHashCode方法,它只计算一次值并将其存储在对象中以便在以后的调用中返回它,那么我必须再次:绝对必须创建一个Equals方法,它将使用存储的比较值,以便A.Equals(B)永远不会从false变为true.否则,合同将被打破.这样做的结果通常是Equals方法没有任何意义 - 它不是原始引用等于,但它也不是一个值等于.有时,这可能是预期的行为(即客户记录),但通常不是.
因此,当对象数据发生更改时,只需更改GetHashCode结果,并且如果使用列表或对象在哈希内部使用对象(或者只是可能),则使对象不可变或创建只读标志以用于包含该对象的散列列表的生命周期.
(顺便说一句:所有这些都不是特定于C#oder的.NET - 它具有所有散列表实现的性质,或者更常见的是任何索引列表的性质,标识对象的数据永远不会改变,而对象在列表中如果这个规则被破坏,就会发生意外和不可预测的行为.在某个地方,可能有列表实现,它会监视列表中的所有元素并自动重新索引列表 - 但这些的性能肯定会令人毛骨悚然.)
答案主要是,它是一个有效的指导方针,但可能不是一个有效的规则.它也没有讲述整个故事.
要点是,对于可变类型,您不能将哈希代码基于可变数据,因为两个相等的对象必须返回相同的哈希代码,并且哈希代码必须在对象的生命周期内有效.如果哈希代码发生更改,您最终会得到一个在哈希集合中丢失的对象,因为它不再存在于正确的哈希箱中.
例如,对象A返回哈希值1.因此,它进入哈希表的bin 1.然后你改变对象A使得它返回2的散列.当散列表寻找它时,它在bin 2中查找并且找不到它 - 该对象在bin 1中是孤立的.这就是散列码必须的原因.不会改变对象的生命周期,只是编写GetHashCode实现的一个原因是对接的痛苦.
更新
Eric Lippert发布了一个博客,提供了很好的信息GetHashCode
.
其他更新
我上面做了一些更改:
我对指南和规则进行了区分.
我突破了"对象的一生".
指南只是一个指南,而不是一个规则.实际上,GetHashCode
当事物期望对象遵循指南时,例如当它存储在哈希表中时,只需要遵循这些准则.如果您从未打算在哈希表(或依赖于规则的任何其他内容)中使用您的对象GetHashCode
,则您的实现不需要遵循指南.
当您看到"对于对象的生命周期"时,您应该阅读"对象需要与哈希表协作的时间"或类似内容.像大多数事情一样,GetHashCode
是关于知道何时违反规则.
来自MSDN
如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值.但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值.
只要没有对对象状态的修改来确定对象的Equals方法的返回值,对象的GetHashCode方法必须始终返回相同的哈希代码.请注意,这仅适用于当前应用程序的执行,并且如果再次运行应用程序,则可以返回不同的哈希代码.
为获得最佳性能,哈希函数必须为所有输入生成随机分布.
这意味着如果对象的值发生变化,则哈希码应该更改.例如,将"Name"属性设置为"Tom"的"Person"类应该有一个哈希码,如果将名称更改为"Jerry",则应该使用不同的代码.否则,汤姆==杰里,这可能不是你想要的.
编辑:
也来自MSDN:
覆盖GetHashCode的派生类也必须重写Equals以保证两个被认为相等的对象具有相同的哈希码; 否则,Hashtable类型可能无法正常工作.
从MSDN的哈希表条目:
密钥对象必须是不可变的,只要它们在Hashtable中用作密钥即可.
我读这个的方式是,可变对象应该在它们的值发生变化时返回不同的哈希码,除非它们被设计用于哈希表.
在System.Drawing.Point的示例中,对象是可变的,并且不返回不同的散列码,当X或Y的值的变化.这将使它成为在哈希表中原样使用的不良候选者.
我认为有关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声明不允许这样做.
然后,如果您有一个实体,您可以实现哈希码,以便它使用该实体的"主要标识符",这可能是理想的代理键或不可变属性.如果您有一个值对象,则可以实现Hashcode,以便它使用该值对象的"属性".这些属性构成了值对象的"定义".这当然是价值对象的本质; 你不是对它的身份感兴趣,而是对它的价值感兴趣.
因此,值对象应该是不可变的.(就像它们在.NET框架中一样,字符串,日期等......都是不可变对象).
另一件事是:
在"会话"期间(我真的不知道应该如何调用它)应该'GetHashCode'返回一个常量值.假设您打开应用程序,从DB(实体)中加载对象的实例,并获取其哈希码.它会返回一定数量.关闭应用程序,然后加载相同的实体.是否要求此次哈希码具有与第一次加载实体时相同的值?恕我直言,不是.
这是个好建议.以下是Brian Pepin就此事所说的话:
这使我不止一次:确保GetHashCode始终在实例的生命周期内返回相同的值.请记住,哈希码用于在大多数哈希表实现中标识"桶".如果对象的"存储桶"发生更改,则哈希表可能无法找到您的对象.这些可能是非常难以找到的错误,所以第一次就把它弄好.
不是直接回答你的问题,但是 - 如果你使用Resharper,不要忘记它有一个为你生成合理的GetHashCode实现(以及Equals方法)的功能.您当然可以指定在计算哈希码时将考虑该类的哪些成员.
看看Marc Brooks的这篇博客文章:
VTO,RTO和GetHashCode() - 哦,我的!
然后查看后续帖子(不能链接,因为我是新的,但是在initlal文章中有一个链接),它进一步讨论并涵盖了初始实现中的一些小缺点.
这是我需要知道的关于创建GetHashCode()实现的所有内容,他甚至提供了他的方法以及其他一些实用程序的下载,简言之.