C # 中的 GetHashCode 准则

我在基本 C # 3.0和.NET 3.5的书中读到:

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

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

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

58614 次浏览

来自 MSDN

如果两个对象相等,则 每个对象的 GetHashCode 方法 必须返回相同的值。但是, 如果两个对象不作为 类的 GetHashCode 方法 两个对象不必返回 不同的价值观。

对象的 GetHashCode 方法 必须始终返回相同的哈希 只要没有 对对象状态的修改 类的返回值 对象的 Equals 方法 仅对当前执行为真 的申请,而有关的申请 可以返回不同的哈希代码 应用程序再次运行。

为了获得最好的性能,一个散列 函数必须生成一个随机 所有输入的分配。

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


编辑 :

同样来自 MSDN:

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

来自 MSDN 的哈希表条目:

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

我对此的理解是,可变对象 应该在其值发生变化时返回不同的哈希码,除非是为在哈希表中使用而设计的。

在系统的例子中。画画。指出,该对象是可变的,当 X 或 Y 值发生变化时,是的返回不同的散列码。这将使它不适合在散列表中使用 as-is。

没有直接回答你的问题,但是——如果你使用 Resharper,不要忘记它有一个特性可以为你生成一个合理的 GetHashCode 实现(以及 Equals 方法)。当然,您可以指定在计算散列码时将考虑到类的哪些成员。

答案主要是,它是一个有效的指导方针,但可能不是一个有效的规则。也不能说明全部。

要指出的是,对于可变类型,您不能将哈希代码基于可变数据,因为两个相等的对象必须返回相同的哈希代码,而且哈希代码必须在对象的生存期内有效。如果哈希代码发生更改,您最终会得到一个在哈希集合中丢失的对象,因为它不再存在于正确的哈希箱中。

例如,对象 A 返回的散列值为1。因此,它进入散列表的第一个容器。然后更改对象 A,使其返回的散列值为2。当一个散列表去寻找它时,它在 bin 2中查找,但是找不到它——该对象在 bin 1中是孤立的。这就是为什么散列代码不能改变 对象的生存期,这也是为什么编写 GetHashCode 实现非常麻烦的一个原因。

更新
Eric Lippert 发布了一个博客 ,为 GetHashCode提供了极好的信息。

更多资料
我在上面做了一些改变:

  1. 我区分了指导方针和规则。
  2. 我突破了“对象的生命周期”。

指南只是指南,不是规则。实际上,GetHashCode只有在对象期望遵循这些指导方针时才必须遵循这些指导方针,例如在将对象存储在散列表中时。如果您从未打算在散列表中使用对象(或者任何其他依赖于 GetHashCode规则的对象) ,那么您的实现就不需要遵循指导原则。

当您看到“ for the life of the object”时,您应该读作“ for the time the object need to co-operation with hash tables”或类似的内容。像大多数事情一样,GetHashCode是关于知道什么时候打破规则。

这是个好建议,以下是布莱恩 · 佩平对此事的看法:

这让我很困惑 Once: 确保始终使用 GetHashCode 方法返回相同的值 记住这一点 散列码用于识别 大多数散列表中的“ bucket” 如果一个对象的 “ bucket”更改后,散列表可能不会更改 能够找到你的对象。这些可以 是很难找到的虫子,所以去找吧 第一次就说对了。

Hashcode 从来没有改变过,但是了解 Hashcode 从何而来也很重要。

如果您的对象使用值语义,也就是说,对象的标识是由它的值(如 String、 Color、所有结构)定义的。如果你的物体的标识与它的所有值无关,那么 Hashcode 就是由它的一部分值来标识的。例如,StackOverflow 条目存储在某个数据库中。如果您更改了您的名称或电子邮件,您的客户条目将保持不变,尽管一些值已经更改(最终您通常由一些较长的客户 ID # 标识)。

简而言之:

值类型语义-Hashcode 由值定义 引用类型语义-Hashcode 由某个 id 定义

我建议你阅读 Eric Evans 的领域驱动设计,如果这还是没有意义的话,他会介绍实体和值类型(这差不多就是我上面尝试做的)。

我认为有关 GetHashcode 的文档有点令人困惑。

一方面,MSDN 规定对象的哈希代码永远不应该更改,而且应该是常量 另一方面,MSDN 还声明,对于2个对象,GetHashcode 的返回值应该相等,如果这2个对象被认为是相等的。

MSDN:

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

  • 如果两个对象相等,则每个对象的 GetHashCode 方法 必须返回相同的值。但是, 如果两个对象不作为 类的 GetHashCode 方法 两个对象不必返回 不同的价值观。
  • 对象的 GetHashCode 方法必须始终如一地返回 只要没有 对对象状态的修改 类的返回值 对象的 Equals 方法 仅对当前执行为真 的申请,而有关的申请 可以返回不同的哈希代码 应用程序再次运行。
  • 为了获得最佳性能,哈希函数必须生成一个随机 所有输入的分配。

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

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

还有一件事:
在这期间,‘ session’(我不知道该如何称呼它)应该返回一个常量值。 假设您打开应用程序,从 DB (一个实体)中加载一个对象的实例,并获得它的散列码。它会返回一定数量。 关闭应用程序,并加载相同的实体。是否要求这次散列码的值与第一次加载实体时的值相同? 恕我直言,没有。

看看马克 · 布鲁克斯(Marc Brooks)的这篇博文:

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

然后查看后续文章(因为我是新来的,所以不能链接,但是在最初的文章中有一个链接) ,其中进一步讨论了最初实现中的一些小缺陷。

这就是我需要了解的关于创建 GetHashCode ()实现的一切,他甚至提供了他的方法的下载和其他一些实用程序,简而言之就是黄金。

虽然已经过去很长时间了,但是我认为仍然有必要对这个问题给出一个正确的答案,包括关于为什么和如何给出的解释。到目前为止最好的答案是一个引用 MSDN 详尽-不要试图制定自己的规则,微软的家伙知道他们在做什么。

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

现在有两个原因

首先为什么 : 如果哈希代码是以某种方式计算的,即在对象的生存期内它不会改变,即使对象本身发生了改变,那么它也会破坏 equals-agreement。

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

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

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

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

根据问题的指导原则,B 的散列码不会改变,结果是: 等于(B) = = > true 但与此同时: A.GetHashCode () = = B.GetHashCode () = = > false.

但是,equals & hashcode 契约明确禁止这种行为。

第二个原因 : 当然,散列码中的更改可能会使用散列码破坏散列表和其他对象,反之亦然。不改变哈希代码在最坏的情况下将得到哈希列表,其中所有不同的对象将有相同的哈希代码,因此在相同的哈希箱-发生时,对象初始化的标准值,例如。


现在来看看怎么做 好吧,乍一看,似乎有一个矛盾——不管怎样,代码都会被破解。 但是这两个问题都不是来自于已更改或未更改的散列码。

这些问题的根源在 MSDN 中得到了很好的描述:

来自 MSDN 的散列表条目:

关键对象必须是不可变的 因为它们被用作 哈希表。

这确实意味着:

任何创建哈希值的对象都应该在对象发生变化时更改哈希值,但是当在哈希表中使用哈希值时(当然也包括其他使用哈希值的对象) ,它绝对不允许对自身进行任何更改。

首先怎么做 当然,最简单的方法是设计不可变对象,仅用于哈希表中,这些哈希表将在需要时作为普通可变对象的副本创建。 在不可变对象内部,显然可以缓存散列码,因为它是不可变的。

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

好吧,我们还应该说: 在某些情况下,有可能拥有具有可变数据的对象,但是散列码没有变化,对象数据发生了变化,而且没有违反 equals & hashcode 契约。

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

所以,当对象数据发生变化时,只需要更改 GetHashCode 的结果,并且如果使用列表或对象的散列中的对象是有意的(或者只是可能的) ,那么使该对象要么是不可变的,要么创建一个只读标志,用于包含该对象的散列列表的生命周期。

(顺便说一下: 所有这些都不是 C # der。NET 特定的-所有哈希表实现,或者更一般的任何索引列表的本质是,当对象在列表中时,对象的标识数据永远不应该改变。如果打破这个规则,就会发生意外和不可预测的行为。在某些地方,可能有列表实现,它们监视列表中的所有元素,并自动重新索引列表——但是这些实现的性能最多只能说是可怕的。)

看看 Eric Lippert 的 GetHashCode 的准则和规则