提问



鉴于以下课程


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很重要?

最佳参考


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


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



  • 如果两件事情相等(Equals(...) == true)那么他们必须为GetHashCode()
  • 返回相同的值
  • 如果GetHashCode()相等,那么它们 是不必要的;这是一次碰撞,并且Equals将被调用以查看它是否是真正的平等。



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


int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;


哦 - 为了方便起见,在覆盖EqualsGetHashCode时,您可能还会考虑提供==!=运算符。





当你弄错了会发生什么事的证明就在这里。

其它参考1


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


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

其它参考2


通过重写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


覆盖Equals()时,请不要忘记检查null的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;
}


原因是:Equalsnull相比必须返回false。另见http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx [45]

其它参考4


怎么样:


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



  假设性能不是问题:)


其它参考5


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

其它参考6


只是添加以上答案:


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


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


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

其它参考7


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



  1. 你不能提供明智的GetHashCode()如果有任何领域
    对象可以改变。通常一个对象永远不会被用在一个
    取决于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;

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

其它参考8


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

其它参考9


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


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


另一方面,如果不能改变价值,可以使用:



   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


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


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

其它参考11


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