【问题标题】:Why is it important to override GetHashCode when Equals method is overridden?为什么在重写 Equals 方法时重写 GetHashCode 很重要?
【发布时间】:2019-02-02 10:02:16
【问题描述】:

给定以下类

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        if (fooItem == null) 
        {
           return false;
        }

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

我已经覆盖了Equals 方法,因为Foo 代表Foos 表的一行。覆盖GetHashCode 的首选方法是什么?

为什么覆盖GetHashCode很重要?

【问题讨论】:

标签: c# overriding hashcode


【解决方案1】:

从 C# 9(.net 5 或 .net core 3.1)开始,您可能希望像使用 Value Based Equality 一样使用 records。

【讨论】:

    【解决方案2】:

    从.NET 4.7 开始,覆盖GetHashCode() 的首选方法如下所示。如果针对较旧的 .NET 版本,请包含 System.ValueTuple nuget 包。

    // C# 7.0+
    public override int GetHashCode() => (FooId, FooName).GetHashCode();
    

    在性能方面,这种方法将优于大多数复合哈希码实现。 ValueTuple 是 struct,所以不会有任何垃圾,并且底层算法是最快的。

    【讨论】:

      【解决方案3】:

      是的,如果您的项目将用作字典中的键或 HashSet<T> 等,这一点很重要 - 因为这是用于(在没有自定义 IEqualityComparer<T> 的情况下)将项目分组到存储桶中。如果两个项目的哈希码不匹配,它们可能永远被认为是相等的(Equals 永远不会被调用)。

      GetHashCode() 方法应该反映Equals 逻辑;规则是:

      • 如果两个事物相等 (Equals(...) == true),那么它们必须为 GetHashCode() 返回相同的值
      • 如果GetHashCode() 相等,则不需要它们必须相同;这是一个冲突,Equals 将被调用以查看它是否是真正的相等。

      在这种情况下,“return FooId;”看起来是一个合适的GetHashCode() 实现。如果您正在测试多个属性,通常使用如下代码组合它们,以减少对角线冲突(即,new Foo(3,5) 与 new Foo(5,3) 具有不同的哈希码):

      unchecked // only needed if you're compiling with arithmetic checks enabled
      { // (the default compiler behaviour is *disabled*, so most folks won't need this)
          int hash = 13;
          hash = (hash * 7) + field1.GetHashCode();
          hash = (hash * 7) + field2.GetHashCode();
          ...
          return hash;
      }
      

      哦 - 为方便起见,您还可以考虑在覆盖 Equals 和 GetHashCode 时提供 == 和 != 运算符。


      here 演示了当您弄错时会发生什么。

      【讨论】:

      • 我能问一下,你是在乘以这些因素吗?
      • 实际上,我可能会失去其中一个;关键是尽量减少冲突的数量 - 使对象 {1,0,0} 具有与 {0,1,0} 和 {0,0,1} 不同的哈希(如果你明白我的意思),
      • 我调整了数字以使其更清晰(并添加了种子)。一些代码使用不同的数字 - 例如 C# 编译器(用于匿名类型)使用 0x51ed270b 的种子和 -1521134295 的因子。
      • @Leandro López:通常将因子选择为素数,因为这样可以减少碰撞次数。
      • “哦 - 为方便起见,您还可以考虑在覆盖 Equals 和 GethashCode 时提供 == 和 != 运算符。”:Microsoft 不鼓励为不可变的对象实现 operator== - msdn.microsoft.com/en-us/library/ms173147.aspx - " 在非不可变类型中重写 operator == 不是一个好主意。"
      【解决方案4】:

      您应该始终保证如果两个对象相等,如 Equals() 定义的那样,它们应该返回相同的哈希码。正如其他一些 cmets 所述,理论上,如果对象永远不会在基于散列的容器(如 HashSet 或 Dictionary)中使用,则这不是强制性的。不过,我建议您始终遵循此规则。原因很简单,因为人们很容易将集合从一种类型更改为另一种类型,以实际提高性能或只是以更好的方式传达代码语义。

      例如,假设我们将一些对象保存在 List 中。一段时间后,有人真正意识到 HashSet 是一个更好的选择,因为它具有更好的搜索特性。这是我们可能遇到麻烦的时候。 List 将在内部使用类型的默认相等比较器,这意味着在您的情况下为 Equals,而 HashSet 使用 GetHashCode()。如果两者的行为不同,您的程序也会如此。请记住,此类问题并非最容易解决。

      我在blog post 中总结了这种行为与其他一些 GetHashCode() 陷阱,您可以在其中找到更多示例和解释。

      【讨论】:

        【解决方案5】:

        我们有两个问题需要解决。

        1. 如果在 对象可以改变。也经常一个对象永远不会在一个 依赖于GetHashCode() 的集合。所以成本 实施GetHashCode() 通常不值得,或者不值得 可能。

        2. 如果有人将您的对象放入一个调用 GetHashCode() 并且您已经覆盖了Equals() 而没有 GetHashCode() 行为正确,那个人可能会花几天时间 追查问题。

        因此默认情况下我会这样做。

        public class Foo
        {
            public int FooId { get; set; }
            public string FooName { get; set; }
        
            public override bool Equals(object obj)
            {
                Foo fooItem = obj as Foo;
        
                if (fooItem == null)
                {
                   return false;
                }
        
                return fooItem.FooId == this.FooId;
            }
        
            public override int GetHashCode()
            {
                // Some comment to explain if there is a real problem with providing GetHashCode() 
                // or if I just don't see a need for it for the given class
                throw new Exception("Sorry I don't know what GetHashCode should do for this class");
            }
        }
        

        【讨论】:

        • 从 GetHashCode 抛出异常违反了 Object 契约。定义GetHashCode 函数没有困难,这样任何两个相等的对象都返回相同的哈希码; return 24601; 和 return 8675309; 都是 GetHashCode 的有效实现。 Dictionary的性能只有在item少的时候表现还不错,如果item变大了会很差,但无论如何都能正常工作。
        • @supercat,如果对象中的标识字段可以更改,则不可能以合理的方式实现 GetHashCode,因为哈希码决不能更改。照你说的做可能会导致某人不得不花费数天时间来追踪性能问题,然后花费数周时间重新设计大型系统以消除对字典的使用。
        • 我曾经为我定义的所有需要​​ Equals() 的类做类似的事情,并且我完全确定我永远不会将该对象用作集合中的键。然后有一天,我使用这样的对象作为 DevExpress XtraGrid 控件的输入的程序崩溃了。事实证明,XtraGrid 在我背后创建了一个 HashTable 或基于我的对象的东西。我与 DevExpress 支持人员就这个问题发生了小争执。我说他们将组件的功能和可靠性建立在未知的客户实现的晦涩方法上,这并不聪明。
        • DevExpress 的人相当尖刻,基本上说我一定是个白痴才能在 GetHashCode() 方法中抛出异常。我仍然认为他们应该找到一种替代方法来做他们正在做的事情-我记得 Marc Gravell 在另一个线程上描述了他如何在不依赖 GetHashCode() 的情况下构建任意对象的字典-不记得他是如何做到的不过。
        • @RenniePet,最好是因为抛出异常而迷恋,然后由于无效的实现而很难找到错误。
        【解决方案6】:

        在覆盖Equals() 时,请不要忘记对照null 检查obj 参数。 并且还要比较类型。

        public override bool Equals(object obj)
        {
            Foo fooItem = obj as Foo;
        
            if (fooItem == null)
            {
               return false;
            }
        
            return fooItem.FooId == this.FooId;
        }
        

        原因是:Equals 在与null 比较时必须返回 false。另见http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

        【讨论】:

        • 在子类引用超类 Equals 方法作为其自身比较的一部分(即 base.Equals(obj))的情况下,此类型检查将失败 - 应改为使用 as
        • @sweetfa:这取决于子类的 Equals 方法是如何实现的。它也可以调用 base.Equals((BaseType)obj)) ,这会正常工作。
        • 不,它不会:msdn.microsoft.com/en-us/library/system.object.gettype.aspx。此外,方法的实现不应失败或成功,具体取决于调用它的方式。如果对象的运行时类型是某个基类的子类,那么无论基类的 Equals() 是如何被调用的,如果 obj 确实等于 this,则基类的 Equals() 应该返回 true。跨度>
        • 将fooItem 移到顶部,然后检查它是否为null,这样在null 或错误类型的情况下会执行得更好。
        • @40Alpha 嗯,是的,那么obj as Foo 将无效。
        【解决方案7】:

        通过覆盖 Equals,您基本上是在说明您是最了解如何比较给定类型的两个实例的人,因此您可能是提供最佳哈希码的最佳人选。

        这是 ReSharper 如何为您编写 GetHashCode() 函数的示例:

        public override int GetHashCode()
        {
            unchecked
            {
                var result = 0;
                result = (result * 397) ^ m_someVar1;
                result = (result * 397) ^ m_someVar2;
                result = (result * 397) ^ m_someVar3;
                result = (result * 397) ^ m_someVar4;
                return result;
            }
        }
        

        如您所见,它只是尝试根据类中的所有字段猜测一个好的哈希码,但由于您知道对象的域或值范围,您仍然可以提供更好的哈希码。

        【讨论】:

        • 这不会总是返回零吗?大概应该将结果初始化为1!还需要多几个分号。
        • 您知道异或运算符 (^) 的作用吗?
        • 正如我所说,这是 R# 在被要求时为你写的(至少它在 2008 年是这样写的)。显然,这个 sn-p 旨在由程序员以某种方式进行调整。至于缺少的分号......是的,当我从 Visual Studio 中的区域选择中复制粘贴代码时,看起来我把它们遗漏了。我还认为人们会弄清楚这两个问题。
        • @SamMackrill 我在缺少的分号中添加了。
        • @SamMackrill 不,它不会总是返回 0。0 ^ a = a,所以0 ^ m_someVar1 = m_someVar1。他还不如将result的初始值设置为m_someVar1。
        【解决方案8】:

        怎么样:

        public override int GetHashCode()
        {
            return string.Format("{0}_{1}_{2}", prop1, prop2, prop3).GetHashCode();
        }
        

        假设性能不是问题:)

        【讨论】:

        • erm - 但是您正在为基于 int 的方法返回一个字符串;_0
        • 不,他确实从 String 对象调用 GetHashCode(),该对象返回一个 int。
        • 我不希望这会像我希望的那样快,不仅是因为涉及值类型的装箱,还有string.Format 的性能。我见过的另一个极客是new { prop1, prop2, prop3 }.GetHashCode()。不能评论这两者之间哪一个会慢一些。不要滥用工具。
        • 这将为{ prop1="_X", prop2="Y", prop3="Z" } 和{ prop1="", prop2="X_Y", prop3="Z_" } 返回true。你可能不希望这样。
        • 是的,您可以随时将下划线符号替换为不常见的符号(例如 •、▲、►、◄、☺、☻),并希望您的用户不要使用这些符号...: )
        【解决方案9】:

        只是补充以上答案:

        如果您不覆盖 Equals,则默认行为是比较对象的引用。这同样适用于哈希码——默认实现通常基于引用的内存地址。 因为您确实覆盖了 Equals 这意味着正确的行为是比较您在 Equals 上实现的任何内容而不是引用,因此您应该对哈希码执行相同的操作。

        您的类的客户会期望哈希码与 equals 方法具有相似的逻辑,例如使用 IEqualityComparer 的 linq 方法首先比较哈希码,只有当它们相等时,他们才会比较 Equals() 方法,这可能运行起来成本更高,如果我们没有实现 hashcode,equal 对象可能会有不同的 hashcode(因为它们有不同的内存地址)并且会被错误地确定为不相等(Equals() 甚至不会命中)。

        此外,除了如果您在字典中使用对象可能无法找到对象的问题(因为它是由一个哈希码插入的,当您查找它时,默认哈希码可能会有所不同,并且再次Equals() 甚至不会被调用,就像 Marc Gravell 在他的回答中解释的那样,您还引入了对字典或哈希集概念的违反,它不应该允许相同的键 - 您已经声明,当您覆盖 Equals 时,这些对象本质上是相同的,因此您不希望它们都作为假设具有唯一键的数据结构上的不同键。但是因为它们具有不同的哈希码,所以“相同”的密钥将作为不同的密钥插入。

        【讨论】:

          【解决方案10】:

          在我看来,考虑到公共属性,下面使用反射是一个更好的选择,因为这样您就不必担心添加/删除属性(尽管不是很常见的情况)。我发现这也表现得更好。(使用 Diagonistics 秒表比较时间)。

              public int getHashCode()
              {
                  PropertyInfo[] theProperties = this.GetType().GetProperties();
                  int hash = 31;
                  foreach (PropertyInfo info in theProperties)
                  {
                      if (info != null)
                      {
                          var value = info.GetValue(this,null);
                          if(value != null)
                          unchecked
                          {
                              hash = 29 * hash ^ value.GetHashCode();
                          }
                      }
                  }
                  return hash;  
              }
          

          【讨论】:

          • GetHashCode() 的实现预计会非常轻量级。我不确定 StopWatch 在数千次调用中是否明显使用反射,但它肯定是数百万次(想想从列表中填充字典)。
          【解决方案11】:

          我的理解是原始的 GetHashCode() 返回对象的内存地址,因此如果您想比较两个不同的对象,则必须重写它。

          编辑: 那是不正确的,原来的 GetHashCode() 方法不能保证 2 个值的相等性。尽管相等的对象返回相同的哈希码。

          【讨论】:

            【解决方案12】:

            哈希代码用于基于哈希的集合,如 Dictionary、Hashtable、HashSet 等。此代码的目的是通过将特定对象放入特定组(桶)来非常快速地对其进行预排序。当您需要从哈希集合中检索该对象时,这种预排序非常有助于找到该对象,因为代码必须仅在一个存储桶中搜索您的对象,而不是在它包含的所有对象中搜索。哈希码分布越好(唯一性越好),检索速度越快。在每个对象都有唯一哈希码的理想情况下,找到它是一个 O(1) 操作。在大多数情况下,它接近 O(1)。

            【讨论】:

              【解决方案13】:

              不一定重要;这取决于您的集合的大小和您的性能要求,以及您的类是否将用于您可能不知道性能要求的库中。我经常知道我的集合大小不是很大,我的时间比创建完美哈希码获得的几微秒性能更有价值;所以(为了摆脱编译器的恼人警告)我只是使用:

                 public override int GetHashCode()
                 {
                    return base.GetHashCode();
                 }
              

              (当然我也可以使用#pragma 来关闭警告,但我更喜欢这种方式。)

              当您处于确实需要性能的位置时,其他人在这里提到的所有问题当然都适用。 最重要的 - 否则在从哈希集或字典中检索项目时会得到错误的结果:哈希码不得随对象的生命周期而变化(更准确地说,在需要哈希码的时候,例如作为字典中的键时):例如,以下是错误的,因为 Value 是公共的,因此可以在实例的生命周期内从外部更改为类,所以您不得将其用作哈希码的基础:

              
                 class A
                 {
                    public int Value;
              
                    public override int GetHashCode()
                    {
                       return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
                    }
                 }    
              

              另一方面,如果 Value 无法更改,则可以使用:

              
                 class A
                 {
                    public readonly int Value;
              
                    public override int GetHashCode()
                    {
                       return Value.GetHashCode(); //OK  Value is read-only and can't be changed during the instance's life time
                    }
                 }
              
              

              【讨论】:

              • 投反对票。这是完全错误的。甚至 Microsoft 在 MSDN (msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx) 中声明,当对象状态以可能影响 Equals() 调用的返回值的方式发生变化时,GetHashCode 的值必须更改,即使在其示例中,它也显示了 GetHashCode 实现完全依赖于公开可变的价值观。
              • Sebastian,我不同意:如果您将对象添加到使用哈希码的集合中,它将被放入依赖于哈希码的 bin 中。如果您现在更改哈希码,您将不会在集合中再次找到该对象,因为将搜索错误的 bin。事实上,这是我们代码中发生的事情,这就是为什么我发现有必要指出这一点。
              • Sebastian,此外,我在链接 (msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx) 中看不到 GetHashCode() 必须更改的声明。相反 - 只要 Equals 为相同的参数返回相同的值,它就不能改变:“只要没有修改确定返回值的对象状态,对象的 GetHashCode 方法就必须始终返回相同的哈希码对象的 Equals 方法。" 这个语句并不意味着相反,如果 Equals 的返回值发生变化,它必须改变。
              • @Joao,您将合同的客户/消费者方面与生产者/实施者混淆了。我说的是实现者的责任,他们重写了 GetHashCode()。您在谈论消费者,即使用价值的人。
              • 完全误解... :) 事实是,当对象的状态改变时,哈希码必须改变,除非状态与对象的身份无关。此外,您永远不应该将 MUTABLE 对象用作集合中的键。为此目的使用只读对象。 GetHashCode、Equals... 和其他一些我现在不记得名字的方法永远不要抛出。
              【解决方案14】:

              实际上很难正确实现GetHashCode(),因为除了Marc 已经提到的规则之外,哈希码在对象的生命周期内不应该改变。因此用于计算哈希码的字段必须是不可变的。

              当我使用 NHibernate 时,我终于找到了解决这个问题的方法。 我的方法是根据对象的 ID 计算哈希码。 ID 只能通过构造函数设置,因此如果您想更改 ID(这不太可能),您必须创建一个具有新 ID 的新对象,因此需要一个新的哈希码。这种方法最适合 GUID,因为您可以提供随机生成 ID 的无参数构造函数。

              【讨论】:

              • @vanja。我相信它与:如果您将对象添加到字典然后更改对象的 id,稍后获取时您将使用不同的哈希来检索它,因此您永远不会从字典中获取它。
              • Microsoft 的 GetHashCode() 函数文档既没有说明也没有暗示对象哈希必须在其生命周期内保持一致。事实上,它专门解释了一种可能不的允许情况:“只要没有对确定对象的 Equals 方法的返回值。"
              • “哈希码在对象的生命周期内不应该改变”——这不是真的。
              • 一个更好的说法是“哈希码(也不是equals的评估)应该在对象用作集合的键期间发生变化”所以如果你将对象添加到字典作为键,您必须确保 GetHashCode 和 Equals 在您从字典中删除对象之前不会更改给定输入的输出。
              • @ScottChamberlain 我想你忘了不在你的评论中,它应该是:“在对象被用作集合的键期间,哈希码(也不等于评估)不应该改变”。对吗?
              【解决方案15】:

              这是因为框架要求两个相同的对象必须具有相同的哈希码。如果重写 equals 方法对两个对象进行特殊比较,并且该方法认为这两个对象相同,那么这两个对象的哈希码也必须相同。 (字典和 Hashtable 依赖于这个原则)。

              【讨论】:

                猜你喜欢
                • 2013-08-06
                • 2011-09-05
                • 2012-10-19
                • 2011-08-16
                • 2017-10-17
                • 2010-11-16
                相关资源
                最近更新 更多