给定以下类别

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


当前回答

只需补充以上答案:

如果不重写Equals,则默认行为是比较对象的引用。这同样适用于哈希码——默认实现通常基于引用的内存地址。因为您确实重写了Equals,这意味着正确的行为是比较您在Equals上实现的任何内容,而不是比较引用,所以您应该对哈希代码执行同样的操作。

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

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

其他回答

我们有两个问题要解决。

如果对象可以更改。此外,对象通常不会用于依赖于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;

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

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

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

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

实际上,要正确实现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;
    }
}

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