c# - 重载equals - 当Equals方法被覆盖时为什么重写GetHashCode很重要?




重载equals '方法后必须重载hashcode '方法 (8)

鉴于以下课程

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


只需添加上面的答案:

如果您不覆盖Equals,那么默认行为是比较对象的引用。 这同样适用于散列码 - 默认实现通常基于引用的内存地址。 因为您确实覆盖了Equals,所以它的正确行为是比较您在Equals上执行的操作,而不是引用,因此您应该对哈希码执行相同的操作。

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

另外,除了如果你在字典中使用它,你可能无法找到你的对象的问题(因为它是由一个散列码插入的,当你查找它时,默认的散列码可能会不同,并且Equals()甚至不会被称为,就像Marc Gravell在他的回答中解释的那样,你还会介绍违反字典或哈希集合概念的问题,它们不应该允许使用相同的键 - 你已经声明这些对象基本上是相同的,因此当你重写Equals时,并不希望它们都是假设拥有唯一键的数据结构上的不同键,但由于它们具有不同的散列码,“相同”键将被插入为不同的键。


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

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


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

  1. 如果可以更改对象中的任何字段,则无法提供明智的GetHashCode() 。 也永远不会在依赖于GetHashCode()的集合中使用对象。 所以实现GetHashCode()的成本往往不值得,或者这是不可能的。

  2. 如果有人将你的对象放入一个调用GetHashCode()的集合中,并且你没有使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");
    }
}

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

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


是的,如果您的项目将用作字典中的键或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


考虑到公共财产,使用反射的下面似乎是一个更好的选择,因为您不必担心增加/移除属性(尽管不是那么常见的情况)。 这发现我的表现也更好。(比较使用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;  
    }

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


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

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





hashcode