首页 文章

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

提问于
浏览
1235

鉴于以下课程

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

12 回答

  • 1157

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

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

  • 9

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

  • 8

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

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

  • -5

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

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

    • 如果有人将您的对象放入一个调用 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");
        }
    }
    
  • 9

    怎么样:

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

    假设性能不是问题:)

  • 125

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

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

  • 0

    只是添加以上答案:

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

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

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

  • 34

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

    是的,重要的是如果您的项目将用作字典中的键,或 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 时提供 ==!= 运算符 .


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

  • 4

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

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

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

    当你处于这样的位置时,你确实需要表现,而不是其他人提到的所有问题 . Most important - 否则在从哈希集或字典中检索项目时会得到错误的结果: the hash code must not vary with the life time of an object (更准确地说,在需要哈希代码的时间,例如在作为字典中的键时):例如,以下是错误,因为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
          }
       }
    
  • 46

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

  • 5

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

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

相关问题