当覆盖 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()
    {
        // Which is preferred?

        return base.GetHashCode();

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

我已经覆盖了Equals方法,因为Foo代表的一排Foo桌上。重写GetHashCode的首选方法是?

重写GetHashCode为什么很重要?

答案

是的,将您的项目用作字典或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时,您可能还考虑提供==!=运算符。


当你得到这个错误会发生什么情况的演示是在这里

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

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

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

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

覆盖Equals()时,请不要忘记检查 obj 参数是否为null 。并比较类型。

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

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

    return fooItem.FooId == this.FooId;
}

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

怎么样:

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

假设性能不是问题:)

我们有两个问题要解决。

  1. 如果可以更改对象中的任何字段,则不能提供合理的GetHashCode() 。通常,在依赖于GetHashCode()的集合中永远不会使用对象。因此,实现GetHashCode()的成本通常不值得,或者不可能实现。

  2. 如果有人将您的对象放入调用GetHashCode()的集合中,并且您在没有使GetHashCode()行为正确的情况下覆盖了Equals() ,则该人可能需要花费数天的时间来查找问题。

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

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");
    }
}

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

只是添加以上答案:

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

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

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

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

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

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