为什么在重写Equals方法时重写GetHashCode很重要?

时间:2008-12-16 13:41:18

标签: c# override hashcode

给出以下课程

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

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

        return fooItem.FooId == this.FooId;
    }

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

        return base.GetHashCode();

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

我已覆盖Equals方法,因为Foo代表Foo表的一行。哪个是覆盖GetHashCode的首选方法?

为什么覆盖GetHashCode很重要?

15 个答案:

答案 0 :(得分:1215)

是的,重要的是,您的项目将用作字典中的密钥,或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;
}

哦 - 为方便起见,您可以考虑在覆盖==!=时提供EqualsGetHashCode运营商。


当你弄错了会发生什么事的证明是here

答案 1 :(得分:126)

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

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

答案 2 :(得分:51)

通过覆盖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;
    }
}

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

答案 3 :(得分:37)

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

public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

原因是:与Equals相比,null必须返回false。另请参阅http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx

答案 4 :(得分:28)

怎么样:

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

假设表现不是问题:)

答案 5 :(得分:10)

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

答案 6 :(得分:10)

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

  1. 如果有任何字段,则无法提供明智的GetHashCode() 对象可以改变。通常也不会在一个对象中使用对象 取决于GetHashCode()的集合。所以费用 实施GetHashCode()通常是不值得的,或者不是 可能的。

  2. 如果有人将您的对象放入调用的集合中 GetHashCode()你已经超越Equals()而没有制作 GetHashCode()行为正确,该人可能会花费数天时间 追查问题。

  3. 因此默认情况下,我这样做。

    public class Foo
    {
        public int FooId { get; set; }
        public string FooName { get; set; }
    
        public override bool Equals(object obj)
        {
            Foo fooItem = obj as Foo;
    
            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");
        }
    }
    

答案 7 :(得分:10)

添加以上答案:

如果不重写Equals,则默认行为是比较对象的引用。这同样适用于hashcode - 默认的implmentation通常基于引用的内存地址。 因为你确实覆盖了Equals,所以它意味着正确的行为是比较你在Equals而不是引用上实现的任何东西,所以你应该对hashcode做同样的事情。

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

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

答案 8 :(得分:8)

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

答案 9 :(得分:5)

这不一定重要;这取决于您的馆藏大小和您的表现要求,以及您的班级是否会在您可能不了解性能要求的图书馆中使用。我经常知道我的集合大小不是很大,而且我的时间比通过创建完美的哈希码获得的几微秒的性能更有价值;所以(为了摆脱编译器的恼人警告)我只是使用:

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

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

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


   class A
   {
      public int Value;

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

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


   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
      }
   }

答案 10 :(得分:5)

.NET 4.7开始,覆盖GetHashCode()的首选方法如下所示。如果定位的是较旧的.NET版本,则包括System.ValueTuple nuget软件包。

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

就性能而言,此方法将胜过大多数 composite 哈希码实现。 ValueTuplestruct,因此不会有任何垃圾,而且底层算法也尽其所能。

答案 11 :(得分:3)

您应始终保证,如果两个对象相等(如Equals()所定义),则它们应返回相同的哈希码。正如其他一些评论所指出的那样,如果从不将对象用于基于哈希的容器(如HashSet或Dictionary)中,则从理论上讲,这不是强制性的。我建议您始终遵循此规则。原因仅仅是因为某人将一个集合从一种类型更改为另一种类型太容易了,其意图是实际上提高性能或只是以更好的方式传达代码语义。

例如,假设我们将一些对象保留在列表中。稍后某个时候,实际上有人意识到HashSet是更好的选择,因为例如更好的搜索特性。这是我们遇到麻烦的时候。列表将在内部使用默认的相等比较器作为类型,这意味着在您的情况下等于,而HashSet使用GetHashCode()。如果两者的行为不同,您的程序也将如此。请记住,此类问题最不容易解决。

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

答案 12 :(得分:2)

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

答案 13 :(得分:1)

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

EDITED: 这是不正确的,原始的GetHashCode()方法无法保证2个值的相等性。虽然相等的对象返回相同的哈希码。

答案 14 :(得分:-4)

以下使用反射在我看来是一个更好的选择考虑公共财产,因为你不必担心添加/删除属性(虽然不常见的情况)。我发现这也表现得更好。(比较时间使用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;  
    }