【问题标题】:GetHashCode Guidelines in C#C# 中的 GetHashCode 指南
【发布时间】:2025-12-26 20:35:16
【问题描述】:

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

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

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

我在 .NET 中尝试了几个内置类型,但它们的行为并非如此。

【问题讨论】:

  • 如果可能,您可能需要考虑更改已接受的答案。

标签: c# .net hashcode


【解决方案1】:

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

但首先要做的是: 问题中引用的指南是错误的。

现在是为什么 - 其中有两个

首先为什么: 如果 hashcode 以某种方式计算,即使对象本身发生变化,它在对象的生命周期内也不会改变,那么它会破坏 equals-contract。

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

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

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

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

根据问题中的指南,B 的哈希码不会改变。结果将是: A.Equals(B) ==> 真 但同时: A.GetHashCode() == B.GetHashCode() ==> 错误。

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

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


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

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

来自 MSDN 的哈希表条目:

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

这确实意味着:

任何创建散列值的对象都应该在对象更改时更改散列值,但是当它在散列表(或任何其他使用散列的对象,当然)。

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

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

好吧,也应该说一下:在某些情况下,可能有具有可变数据的对象,其中哈希码仍然未更改,当对象数据更改时,不会违反 equals&hashcode-contract。

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

所以,只要让 GetHashCode 结果改变,当对象数据改变时,并且如果使用列表或对象的散列内的对象的使用是有意的(或只是可能的)然后使对象不可变或创建一个只读标志用于包含对象的哈希列表的生命周期。

(顺便说一句:所有这些都不是 C# 或 .NET 特定的 - 它是所有哈希表实现的本质,或更一般地任何索引列表的本质,对象的标识数据永远不应该改变,而对象是在列表中。如果违反此规则,将发生意外和不可预测的行为。在某个地方,可能存在列表实现,它会监视列表中的所有元素并自动重新索引列表 - 但这些实现肯定会令人毛骨悚然最好。)

【讨论】:

  • +1 详细解释(如果可以的话,我会提供更多)
  • +1 由于冗长的解释,这绝对是更好的答案! :)
【解决方案2】:

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

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

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

更新
Eric Lippert has posted a blog 提供有关GetHashCode 的出色信息。

其他更新
我在上面做了一些更改:

  1. 我区分了准则和规则。
  2. 我贯穿了“对象的整个生命周期”。

指南只是一个指南,而不是规则。实际上,GetHashCode 只有在期望对象遵循这些准则时才需要遵循这些准则,例如当它被存储在哈希表中时。如果您从不打算在哈希表中使用您的对象(或任何其他依赖于 GetHashCode 规则的东西),那么您的实现不需要遵循指南。

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

【讨论】:

  • 如何确定可变类型之间的相等性?
  • 您不应该使用 GetHashCode 来确定相等性。
  • @JS Bangs - 来自 MSDN:重写 GetHashCode 的派生类也必须重写 Equals 以保证两个被认为相等的对象具有相同的哈希码;否则,Hashtable 类型可能无法正常工作。
  • @Joan Venge:两件事。首先,即使是 Microsoft 也没有在每个实现中都正确使用 GetHashCode。其次,值类型通常是不可变的,每个值都是一个新实例,而不是对现有实例的修改。
  • 由于 a.Equals(b) 必须意味着 a.GetHashCode() == b.GetHashCode(),如果用于相等比较的数据发生更改,则哈希码通常必须更改。我会说问题不在于 GetHashCode 基于可变数据。问题是使用可变对象作为哈希表键(实际上是对它们进行变异)。我错了吗?
【解决方案3】:

来自MSDN

如果两个对象比较相等,则 每个对象的 GetHashCode 方法 必须返回相同的值。然而, 如果两个对象不比较为 相等,GetHashCode 方法为 两个对象不必返回 不同的价值观。

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

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

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


编辑

同样来自 MSDN:

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

来自MSDN's hashtable entry

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

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

在 System.Drawing.Point 的例子中,对象是可变的,当 X 或 Y 值改变时确实返回不同的哈希码。这将使它不适合在哈希表中按原样使用。

【讨论】:

  • GetHashCode() 设计用于哈希表,这是此函数的唯一要点。
  • @skolima - MSDN 文档与此不一致。可变对象可以实现 GetHashCode(),并且应该随着对象值的变化返回不同的值。哈希表必须使用不可变键。因此,您可以将 GetHashCode() 用于哈希表以外的其他内容。
【解决方案4】:

我认为有关 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 中的规则。 假设您有 2 个此类的实例; instance1 的 Name 属性设置为“Pol”,instance2 的 Name 属性设置为“Piet”。 两个实例都返回不同的哈希码,而且它们也不相等。 现在,假设我将 instance2 的 Name 更改为 'Pol',那么根据我的 Equals 方法,两个实例应该相等,并且根据 MSDN 的规则之一,它们应该返回相同的哈希码。
但是,这是无法做到的,因为 instance2 的 hashcode 会发生变化,而 MSDN 声明这是不允许的。

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

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

【讨论】:

  • 您的示例就是为什么 Jeff Yates 说您不能将哈希码基于可变数据。如果哈希码基于该对象的可变值,您不能将可变对象粘贴到 Dictionary 中并期望它能够正常工作。
  • 我看不到哪里违反了 MSDN 规则?该规则明确规定:对象的 GetHashCode 方法必须始终返回相同的哈希码,只要确定对象的 Equals 方法的返回值的对象状态没有修改。这意味着当您将instance2的Name更改为Pol时,允许更改instance2的hashcode
【解决方案5】:

这是个好建议。以下是 Brian Pepin 对此事的看法:

这让我绊倒了不止 一次:确保始终使用 GetHashCode 返回相同的值 实例的生命周期。请记住 哈希码用于识别 大多数哈希表中的“桶” 实施。如果一个对象的 “桶”的变化,哈希表可能不会 能够找到你的对象。这些可以 很难找到错误,所以得到它 第一次就对了。

【讨论】:

  • 我没有投反对票,但我猜其他人投了反对票,因为它的引用并没有涵盖整个问题。假装字符串是可变的,但没有改变哈希码。您创建“bob”,将其用作哈希表中的键,然后将其值更改为“phil”。接下来创建一个新字符串“phil”。如果您随后查找带有键“phil”的哈希表条目,则不会找到您最初放入的项目。如果有人在“bob”上搜索它会被找到,但你会得到一个可能不再正确的值。要么努力不使用可变密钥,要么注意危险。
  • @EricTuttleman:如果我为框架编写规则,我会指定对于任何一对对象XY,一旦调用了X.Equals(Y)Y.Equals(X),所有未来的调用都应该产生相同的结果。如果想使用其他的相等定义,请使用EqualityComparer<T>
【解决方案6】:

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

【讨论】:

  • 谢谢,实际上我从来没有使用过 Resharper,但我经常看到它被提及,所以我应该试一试。
  • +1 Resharper 如果有的话,它会生成一个不错的 GetHashCode 实现。
【解决方案7】:

查看 Marc *s 的这篇博文:

VTOs, RTOs and GetHashCode() -- oh, my!

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

这是我创建 GetHashCode() 实现所需了解的所有内容,他甚至提供了他的方法以及其他一些实用程序的下载,简而言之就是黄金。

【讨论】:

    【解决方案8】:

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

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

    简而言之:

    值类型语义 - 哈希码由值定义 引用类型语义 - Hashcode 由一些 id 定义

    如果这仍然没有意义,我建议您阅读 Eric Evans 的域驱动设计,他在其中探讨了实体与值类型(这或多或少是我在上面尝试做的)。

    【讨论】:

    • 这不太正确。对于特定实例,哈希码必须保持不变。在值类型的情况下,通常每个值都是一个唯一的实例,因此散列似乎发生了变化,但实际上它是一个新实例。
    • 你是对的,值类型是不可变的,所以它们排除了改变。很好的收获。
    【解决方案9】:

    查看 Eric Lippert 的 Guidelines and rules for GetHashCode

    【讨论】: