给定以下类别

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表示Foos表的一行。哪个是重写GetHashCode的首选方法?

为什么重写GetHashCode很重要?


当前回答

从C#9(.net5或.netcore3.1)开始,您可能希望使用记录,因为默认情况下它使用基于值的相等。

其他回答

重写Equal()时,请不要忘记检查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

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

GetHashCode()方法应反映Equals逻辑;规则如下:

如果两个值相等(Equals(…)==true),则它们必须为GetHashCode()返回相同的值如果GetHashCode()相等,则它们不必相同;这是一个冲突,将调用Equals来查看它是否是真正的相等。

在本例中,看起来“return FooId;”是一个合适的GetHashCode()实现。如果您正在测试多个财产,通常使用如下代码组合它们,以减少对角线冲突(即,使新的Foo(3,5)具有与新Foo(5,3)不同的哈希代码):

在现代框架中,HashCode类型具有帮助您从多个值创建哈希代码的方法;在较旧的框架上,您需要不使用,因此类似于:

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

哦-为了方便,您也可以考虑提供==和!=运算符。


当你犯了这个错误时会发生什么。

在我看来,考虑到公共财产,下面使用反射似乎是一个更好的选择,因为在此情况下,您不必担心财产的添加/删除(尽管不太常见)。我发现这也表现得更好。(使用诊断学秒表比较时间)。

    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,这意味着正确的行为是比较您在Equals上实现的任何内容,而不是比较引用,所以您应该对哈希代码执行同样的操作。

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

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

您应该始终保证,如果两个对象相等(由Equals()定义),它们应该返回相同的哈希代码。正如其他一些评论所说,理论上,如果对象永远不会在HashSet或Dictionary等基于哈希的容器中使用,则这不是强制性的。但我建议你始终遵守这条规则。原因很简单,因为人们很容易将集合从一种类型更改为另一种类型,以便实际提高性能或以更好的方式传递代码语义。

例如,假设我们在列表中保留一些对象。一段时间后,有人意识到HashSet是一个更好的选择,因为它具有更好的搜索特性。这是我们可能陷入麻烦的时候。List将在内部为类型使用默认的相等比较器,在您的情况下,这意味着Equals,而HashSet使用GetHashCode()。如果两者的行为不同,那么您的程序也会不同。请记住,这些问题不是最容易解决的。

我在一篇博客文章中总结了这种行为和其他GetHashCode()陷阱,在这里可以找到更多的示例和解释。