【问题标题】:General advice and guidelines on how to properly override object.GetHashCode()关于如何正确覆盖 object.GetHashCode() 的一般建议和指南
【发布时间】:2010-11-25 14:35:06
【问题描述】:

根据MSDN,哈希函数必须具有以下属性:

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

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

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


我不断发现自己处于以下情况:我创建了一个类,实现了IEquatable<T> 并覆盖了object.Equals(object)MSDN 声明:

覆盖 Equals 的类型也必须覆盖 GetHashCode ;否则,Hashtable 可能无法正常工作。

然后它通常对我来说有点停顿。因为,您如何正确覆盖object.GetHashCode()?永远不知道从哪里开始,而且似乎有很多陷阱。

在 StackOverflow 上,有很多与 GetHashCode 覆盖相关的问题,但其中大多数似乎是针对特定情况和特定问题的。所以,因此我想在这里得到一个很好的编译。包含一般建议和指南的概述。做什么,不做什么,常见的陷阱,从哪里开始等等。

我希望它特别针对 C#,但我认为它也适用于其他 .NET 语言(?)。


我认为也许最好的方法是为每个主题创建一个答案,首先是一个快速而简短的答案(如果可能的话,接近单行),然后可能会提供更多信息并以相关问题、讨论结束,博客文章等,如果有的话。然后,我可以仅使用“目录”创建一个帖子作为接受的答案(将其放在首位)。尽量保持简短和简洁。不要只链接到其他问题和博客文章。尝试抓住它们的本质,然后链接到源(特别是因为源可能会消失。另外,请尝试编辑和改进答案,而不是创建许多非常相似的答案。

我不是一个很好的技术作家,但我至少会尝试格式化答案,使它们看起来相似,创建目录等。我也会尝试在这里搜索一些相关问题在 SO 这回答了其中的部分问题,并且可能会提取出我可以管理的那些本质。但是由于我在这个话题上不是很稳定,所以我会尽量远离:p

【问题讨论】:

    标签: c# .net hashcode gethashcode


    【解决方案1】:
    【解决方案2】:

    查看 Eric Lippert 的 Guidelines and rules for GetHashCode

    【讨论】:

      【解决方案3】:

      目录


      我想介绍但尚未介绍的内容:

      • 如何创建整数(无论如何,如何将对象“转换”为 int 对我来说并不是很明显)。
      • 哈希码基于哪些字段。
        • 如果它应该只在不可变字段上,如果只有可变字段怎么办?
      • 如何生成良好的随机分布。 (MSDN 属性 #3)
        • 除此之外,似乎选择了一个很好的魔法素数(见过 17、23 和 397),但你是如何选择它的,它到底有什么用?
      • 如何确保哈希码在整个对象生命周期内保持不变。 (MSDN 属性 #2)
        • 尤其是当相等基于可变字段时。 (MSDN 属性 #1)
      • 如何处理复杂类型的字段(不在built-in C# types 中)。
        • 复杂的对象和结构、数组、集合、列表、字典、泛型类型等。
        • 例如,即使列表或字典可能是只读的,但这并不意味着它的内容是只读的。
      • 如何处理继承的类。
        • 您是否应该以某种方式将base.GetHashCode() 合并到您的哈希码中?
      • 从技术上讲,你能不能只是懒惰并返回 0?会严重违反 MSDN 准则 #3,但至少会确保 #1 和 #2 始终正确:P
      • 常见的陷阱和陷阱。

      【讨论】:

        【解决方案4】:

        GetHashCode 实现中常见的神奇数字是什么?

        它们是质数。素数用于创建哈希码,因为素数可以最大限度地利用哈希码空间。

        具体来说,从小素数3开始,只考虑结果的低位nybbles

        • 3 * 1 = 3 = 3(mod 8) = 0011
        • 3 * 2 = 6 = 6(mod 8) = 1010
        • 3 * 3 = 9 = 1(mod 8) = 0001
        • 3 * 4 = 12 = 4(mod 8) = 1000
        • 3 * 5 = 15 = 7(mod 8) = 1111
        • 3 * 6 = 18 = 2(mod 8) = 0010
        • 3 * 7 = 21 = 5(mod 8) = 1001
        • 3 * 8 = 24 = 0(mod 8) = 0000
        • 3 * 9 = 27 = 3(mod 8) = 0011

        然后我们重新开始。但是你会注意到,在开始重复之前,我们素数的连续倍数在我们的 nybble 中产生了所有可能的位排列。我们可以使用任何素数和任意位数获得相同的效果,这使得素数最适合生成近随机哈希码。在上面的例子中,我们通常看到更大的素数而不是像 3 这样的小素数的原因是,对于我们的哈希码中的更多位数,使用小素数获得的结果甚至不是伪随机的——它们只是一个递增序列直到遇到溢出。为了获得最佳随机性,应使用会导致相当小的系数溢出的素数,除非您可以保证您的系数不会很小。

        相关链接:

        【讨论】:

          【解决方案5】:

          A) 如果要使用值相等而不是默认引用相等,则必须同时覆盖 Equals 和 GetHashCode。对于后者,如果两个对象引用都引用同一个对象实例,则它们比较相等。与前者相比,如果它们的值相同,即使它们引用不同的对象,它们也是相等的。例如,您可能希望对 Date、Money 和 Point 对象使用值相等。

          B) 为了实现值相等,您必须重写 Equals 和 GetHashCode。两者都应该依赖于封装值的对象的字段。例如,Date.Year、Date.Month 和 Date.Day;或 Money.Currency 和 Money.Amount;或 Point.X、Point.Y 和 Point.Z。您还应该考虑覆盖运算符 ==、运算符 !=、运算符 。

          C) 哈希码不必在对象生命周期中始终保持不变。但是,当它作为密钥参与散列时,它必须保持不可变。来自 MSDN doco for Dictionary:“只要将对象用作 Dictionary)>) 中的键,它就不能以任何影响其哈希值的方式发生变化。”如果您必须更改键的值,请从字典中删除条目,更改键值并替换条目。

          D) IMO,如果您的价值对象本身是不可变的,您将简化您的生活。

          【讨论】:

            【解决方案6】:
            public override int GetHashCode()
            {
                return IntProp1 ^ IntProp2 ^ StrProp3.GetHashCode() ^ StrProp4.GetHashCode ^ CustomClassProp.GetHashCode;
            }
            

            在 customClass 的 GetHasCode 方法中执行相同的操作。像魅力一样工作。

            【讨论】:

            • 为什么?它背后的理论是什么?你怎么能确定它“像魅力一样工作”?
            • 我认为假设是微软在String类中正确实现了GetHashCode(),并且由于它们的hashcode是唯一的,所以这个操作也应该返回一个唯一的值。
            • 这种方法的问题是相等的属性产生相同的哈希码。当您对这些哈希码进行异或运算时,您会得到0。示例:ideone.com/vndV1H
            【解决方案7】:

            如何确保哈希码在对象的整个生命周期内保持不变。 (MSDN 属性 #2)尤其是当相等基于可变字段时。 (MSDN 属性 #1)

            您似乎误解了属性 #2。哈希码不需要在对象的整个生命周期中保持不变。只要确定 equals 方法结果的值没有改变,它就需要保持不变。因此,从逻辑上讲,您仅将哈希码基于这些值。那么应该问题不大。

            【讨论】:

            • 嗯...不确定我是否跟随。如果用作键的对象突然改变了它的哈希码,你不会“丢失”一个对象吗?
            • 好吧,我认为您不应该更改用作键的对象。所以从这个意义上说,最好将散列基于不可变字段。但是属性 #2 并没有说明这一点。这不是必需的。
            • 除了名称之外,这是一个要求。确实,稳定的哈希码只是哈希表(和其他集合)实现的要求,而不是接口本身的要求,但这是一个非常强的要求。要求知道没有对您的对象的引用存储在哈希表中,或者您正在更改的属性不会更改哈希似乎是一个非常繁重的问题(或公然无视封装)。所以一个不可变的哈希表是一个实际的要求,即使不是严格意义上的 GetHashCode doco 要求。
            • 我认为您不应该在哈希表中使用这样的对象。如果对象以这样一种方式更改,即等于方法更改,那么您不会期望它获得相同的哈希值。假设您有一个类 Point 有两个字段 XY。假设您不要让这个类在性能或其他方面是不可变的。所以XY 有公共设置器。现在您必须将哈希基于可变字段。
            【解决方案8】:

            哈希码基于哪些字段?如果它应该只在不可变字段上,如果只有可变字段怎么办?

            它不需要仅基于不可变字段。我将它基于确定 equals 方法结果的字段。

            【讨论】:

            • 将其基于不可变字段是一个好主意,因为 Svish 提到的原因是:“如果用作键的对象突然更改了其哈希码,则您“丢失”了一个对象”。但我认为这取决于您对对象的目的。
            • GetHashCode() 主要用于哈希表。 “您不应该在可变引用类型上覆盖 Equals。这是因为覆盖 Equals 要求您还覆盖 GetHashCode 方法,如上一节所述。这意味着可变引用类型实例的哈希码在它的生命周期,这可能导致对象在哈希表中丢失。” docs.microsoft.com/en-us/dotnet/api/…
            【解决方案9】:

            为什么我必须覆盖object.GetHashCode()

            重写此方法很重要,因为以下属性必须始终保持为真:

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

            正如JaredParblog post 中关于实现平等所述,原因在于

            许多类使用哈希码对对象进行分类。特别是哈希表和字典倾向于根据它们的哈希码将对象放在桶中。当检查一个对象是否已经在哈希表中时,它将首先在存储桶中查找它。如果两个对象相等但具有不同的哈希码,则它们可能会被放入不同的桶中,并且字典将无法查找该对象。

            相关链接:

            【讨论】:

              【解决方案10】:

              什么时候覆盖object.GetHashCode()

              正如MSDN 所说:

              覆盖 Equals 的类型也必须覆盖 GetHashCode ;否则,Hashtable 可能无法正常工作。

              相关链接:

              【讨论】:

                【解决方案11】:

                只要您对该类型的对象具有有意义的相等度量(即您覆盖 Equals),就应该覆盖它。如果您知道该对象不会因任何原因被散列,您可以离开它,但您不太可能提前知道这一点。

                哈希应该仅基于用于定义相等的对象的属性,因为被认为相等的两个对象应该具有相同的哈希码。一般来说,您通常会执行以下操作:

                
                public override int GetHashCode()
                {
                    int mc = //magic constant, usually some prime
                    return mc * prop1.GetHashCode() * prop2.GetHashCode * ... * propN.GetHashCode();
                }
                

                我通常假设将这些值相乘会产生一个相当均匀的分布,假设每个属性的哈希码函数都是一样的,尽管这很可能是错误的。使用这种方法,如果对象相等定义属性发生变化,那么哈希码也可能发生变化,这是可以接受的,因为您的问题中的定义 #2。它还以统一的方式处理所有类型。

                您可以为所有实例返回相同的值,尽管这会使任何使用散列的算法(例如字典)非常慢 - 基本上所有实例都将散列到同一个存储桶,然后查找将变为 O(n)的预期 O(1)。这当然否定了使用此类结构进行查找的任何好处。

                【讨论】:

                • 我可能遗漏了一些东西,但是如果您在不同的属性中具有相同的值?例如。当 objA.prop1 == objB.prop2 和 objA.prop2 == objB.prop1 但 objA.prop1 != objA.prop2。您可以连接字符串值,然后对其进行哈希处理。
                • @Eric - 是的,您可以这样做,尽管上述方法没有任何问题,因为虽然 objA 和 objB 具有相同的哈希码,但无需确保具有相同哈希码的对象相等, 正好相反。例如,您的方法可能会为 HashTable 带来更好的性能,并且对于您给出的情况可能会更好。
                猜你喜欢
                • 2019-10-30
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-01-08
                • 1970-01-01
                相关资源
                最近更新 更多