题 为什么在重写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()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

我已经超越了 Equals 方法因为 Foo 代表一排 Foos表。哪个是覆盖的首选方法 GetHashCode

为什么重写是很重要的 GetHashCode


1167
2017-12-16 13:41


起源


由于冲突,特别是在使用字典时,实现equals和gethashcode非常重要。如果两个对象返回相同的哈希码,它们将被链接插入字典中。在访问项目时使用equals方法。 - DarthVader


答案:


是的,重要的是,您的项目将用作字典中的键,或者 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;

哦 - 为方便起见,您也可以考虑提供 == 和 != 操作员在覆盖时 Equals 和 GetHashCode


证明当你弄错了会发生什么 这里


1101
2017-12-16 13:47



我可以问,你是否会成倍增加这些因素? - Leandro López
实际上,我可能会失去其中一个;关键是尽量减少碰撞次数 - 这样对象{1,0,0}就会有{0,1,0}和{0,0,1}的不同哈希(如果你明白我的意思) ) - Marc Gravell♦
我调整了数字以使其更清晰(并添加了种子)。某些代码使用不同的数字 - 例如,C#编译器(对于匿名类型)使用0x51ed270b的种子和-1521134295的因子。 - Marc Gravell♦
@LeandroLópez:通常选择因子是素数,因为它会使碰撞次数变小。 - Andrei Rînea
“哦 - 为方便起见,你可能还会考虑在重写Equals和GethashCode时提供==和!=运算符。”:Microsoft不鼓励对不可变的对象实现operator == - msdn.microsoft.com/en-us/library/ms173147.aspx  - “在非不可变类型中覆盖operator ==不是一个好主意。” - antiduh


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

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


115
2017-12-21 12:39



@vanja。我相信它与以下内容有关:如果您将对象添加到字典中然后更改对象的id,则稍后提取时您将使用不同的哈希来检索它,因此您永远不会从字典中获取它。 - ANeves
Microsoft的GetHashCode()函数文档既没有声明也没有暗示对象哈希必须在其生命周期内保持一致。事实上,它具体解释了一个允许的案例 不:“对象的GetHashCode方法必须始终返回相同的哈希码,只要不对对象状态进行修改即可确定对象的Equals方法的返回值。” - PeterAllenWebb
“哈希代码不应该在对象的生命周期中改变” - 这不是真的。 - zgnilec
更好的说法是“哈希码(也不是平等的评价)应该在对象被用作集合的键的时间段内改变”所以如果你把对象作为键添加到字典中,你必须确保在从字典中删除对象之前,GetHashCode和Equals不会更改给定输入的输出。 - Scott Chamberlain
@ScottChamberlain我认为你忘记了你的评论,它应该是:“在对象被用作集合的关键时期,哈希码(以及等于的平均值)不应该改变”。对? - Stan Prokop


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

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


41
2017-12-16 13:48



这不会总是归零吗?可能应该将结果初始化为1!还需要更多的分号。 - Sam Mackrill
您知道XOR运算符(^)的作用吗? - Stephen Drew
正如我所说的那样,这就是R#为你写的东西(至少它是2008年所做的)。显然,这个片段是由程序员以某种方式调整的。至于缺少的冒号......是的,当我从Visual Studio中的区域选择中复制粘贴代码时,看起来我将它们遗漏了。我还以为人们会把它们弄清楚。 - Trap
@SamMackrill我添加了遗漏的分号。 - Matthew Murdoch
@SamMackrill不,它不会总是返回0。 0 ^ a = a所以 0 ^ m_someVar1 = m_someVar1。他也可以设定初始值 result 至 m_someVar1。 - Millie Smith


请不要忘记检查obj参数 null 什么时候覆盖 Equals()。 并且还比较类型。

public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

原因是: Equals 必须返回false作为比较 null。也可以看看 http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx


32
2017-11-17 07:46



在子类引用超类Equals方法作为其自身比较的一部分(即base.Equals(obj))的情况下,对类型的检查将失败 - 应该用作替代 - sweetfa
@sweetfa:这取决于如何实现子类的Equals方法。它也可以调用base.Equals((BaseType)obj)),它可以正常工作。 - huha
不,它不会: msdn.microsoft.com/en-us/library/system.object.gettype.aspx。此外,方法的实现不应该失败或取决于它的调用方式。如果对象的运行时类型是某个基类的子类,则基类的Equals()应返回true,如果 obj 确实等于 this 无论如何调用基类的Equals()。 - Jupiter
移动 fooItem 在null或错误类型的情况下,在顶部然后检查它将为null。 - IllidanS4
@ 40Alpha嗯,是的,那么 obj as Foo 会无效的。 - IllidanS4


怎么样:

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

假设性能不是问题:)


23
2017-11-25 00:48



嗯 - 但是你要为基于int的方法返回一个字符串; _0 - jim tollan
不,他确实从String对象调用GetHashCode(),该对象返回一个int。 - Richard Clayton
我不希望这会像我想的那样快,不仅仅是为了价值类型所涉及的拳击,而且还为了表现 string.Format。我见过的另一个令人讨厌的是 new { prop1, prop2, prop3 }.GetHashCode()。不能评论哪一个在这两者之间会更慢。不要滥用工具。 - nawfal
这将返回true { prop1="_X", prop2="Y", prop3="Z" } 和 { prop1="", prop2="X_Y", prop3="Z_" }。你可能不希望这样。 - voetsjoeba
是的,您总是可以用不常见的东西替换下划线符号(例如•,▲,►,◄,,☻),并希望您的用户不会使用这些符号...... :) - Ludmil Tinkov


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


9
2017-12-16 13:48





只是添加以上答案:

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

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

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


8
2017-11-12 13:48