当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《为什么在重写Equals方法时重写GetHashCode很重要?》经验,为你挑选了10个好方法。

鉴于以下课程

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



1> Marc Gravell..:

是的,重要的是,您的项目将用作字典中的键,或者HashSet等等 - 因为这是用于(在没有自定义的情况下IEqualityComparer)将项目分组到存储桶中.如果两个项的哈希码不匹配,它们可能永远不会被认为是相等的(Equals将永远不会被调用).

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

如果两个东西相等(Equals(...) == true)那么它们必须返回相同的值GetHashCode()

如果GetHashCode()是相等的,它是必要对他们是相同的; 这是一次碰撞,Equals将被调用以查看它是否是真正的平等.

在这种情况下,看起来" return FooId;"是一个合适的GetHashCode()实现.如果您正在测试多个属性,通常使用下面的代码组合它们,以减少对角线冲突(即,new Foo(3,5)具有不同的哈希码new Foo(5,3)):

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

哦 - 为了方便起见,你也可以考虑提供==!=操作员覆盖EqualsGetHashCode.


当你弄错了会发生什么事的证明就在这里.


@LeandroLópez:通常选择因子是素数,因为它会使碰撞次数变小.
我可以问你是否会成倍增加这些因素?
"哦 - 为了方便起见,你可能还会考虑在重写Equals和GethashCode时提供==和!=运算符.":Microsoft不鼓励对不可变的对象实现operator == - http://msdn.microsoft.com/en- us/library/ms173147.aspx - "在非不可变类型中覆盖operator ==不是一个好主意."
实际上,我可能会失去其中一个; 重点是尽量减少碰撞次数 - 这样对象{1,0,0}就会有{0,1,0}和{0,0,1}的不同哈希值(如果你明白我的意思) )
我调整了数字以使其更清晰(并添加了种子).某些代码使用不同的数字 - 例如,C#编译器(对于匿名类型)使用0x51ed270b的种子和-1521134295的因子.
次要注意事项 - 如果启用了/ checked项目编译器设置,则计算哈希的这种特定实现很容易产生整数溢出异常.考虑使用未经检查的{}包装计算
@antiduh通过该标记,对于不可变类型混淆GetHashCode也不是一个好主意,这意味着不要乱用Equals!所以:如果它不是价值风格(无论是`class` vs`class`),不要乱平等 - 这是一个合理的经验法则.
@RGI更一般地说,'AppDomain`之间永远不能保证`GetHashCode()`,所以这不是一个重要的声明; 其余的只是对答案中第二颗子弹的重新陈述.默认实现是找到*哈希*,但不能用作*唯一标识符* - 我们已经说过的所有内容.
@grzegorz_p是倒退的; 两个具有相同哈希码的东西**并不意味着它们是相等的(`Equals` == true),但是,两个报告相等的东西(`Equals` == true)**必须**返回相同的哈希码,否则他们打破了API.你确定你不是简单地误读段落吗?引用MSDN:"如果在测试两个对象的相等性时,重写的Equals方法返回true,则重写的GetHashCode方法必须为两个对象返回相同的值."
@Richard不,不应该
@Neil公平观察; 未选中是编译器默认值,但您是对的:不应该假设它
@Lijo是的,你应该
@ user45623不,它不会-`int field1 = 1,field2 = 2; // 646`-`int field1 = 2,field2 = 1; // 652`; 此外,碰撞是“不方便的”,但不是“无效的” *

2> Albic..:

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

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


Microsoft的GetHashCode()函数文档既没有声明也没有暗示对象哈希在其生命周期内必须保持一致.事实上,它特别解释了一个允许的情况,它可能*不*:"对象的GetHashCode方法必须始终返回相同的哈希码,只要没有对对象状态的修改来确定对象的返回值等于方法."
"哈希代码不应该在对象的生命周期中改变" - 这不是真的.
@vanja.我相信它与以下内容有关:如果您将对象添加到字典中然后更改对象的id,则稍后提取时您将使用不同的哈希来检索它,因此您永远不会从字典中获取它.
@ScottChamberlain我认为你忘了不在你的评论中,它应该是:"在对象被用作集合的关键时期,哈希码(以及等于的平均值)不应该改变".对?
更好的说法是"哈希码(也不是平等的评价)应该在对象被用作集合的关键时期内改变"所以如果你把对象作为键添加到字典中,你必须确保在从字典中删除对象之前,GetHashCode和Equals不会更改给定输入的输出.
你能详细说明"哈希码在对象的生命周期内不应该改变"吗?这个NHibernate具体吗?
@PeterAllenWebb:.net的设计融合了Java的许多方面,包括一些错误.由于Java集合的设计不允许用户指定备用比较方法和散列函数,因此集合将两个对象视为彼此匹配的唯一方法是让它们将自己报告为"相等",无论它们是否实际等效.这导致不同类型将"等于"定义为意味着截然不同的事物,这使得封装或聚合泛型类型的其他对象的对象很难知道它们是否"相等".

3> Trap..:

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

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


您知道XOR运算符(^)的作用吗?
这不会总是归零吗?可能应该将结果初始化为1!还需要更多的分号.
@SamMackrill不,它不会总是返回0.`0 ^ a = a`,所以`0 ^ m_someVar1 = m_someVar1`.他也可以将`result`的初始值设置为`m_someVar1`.
@SamMackrill我添加了丢失的分号.

4> huha..:

请不要忘记null在覆盖时检查obj参数Equals().并且还比较了类型.

public override bool Equals(object obj)
{
    Foo fooItem = obj as Foo;

    if (fooItem == null)
    {
       return false;
    }

    return fooItem.FooId == this.FooId;
}

原因是:Equals必须在比较时返回false null.另请参见http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx


这种检查的类型将在一个子类是指超的局面失败equals方法,因为它自己比较的一部分(即base.Equals(OBJ)) - 应改用
不,它不会:http://msdn.microsoft.com/en-us/library/system.object.gettype.aspx.此外,方法的实现不应该失败或取决于它的调用方式.如果对象的运行时类型是某个基类的子类,那么如果`obj`确实等于`this`,则无论基类的Equals()如何被调用,基类的Equals()都应该返回true.
将`fooItem`移动到顶部然后将其检查为null将在null或错误类型的情况下执行得更好.

5> Ludmil Tinko..:

怎么样:

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

假设性能不是问题:)


不,他确实从String对象调用GetHashCode(),该对象返回一个int.
对于`{prop1 ="_ X",prop2 ="Y",prop3 ="Z"}和`{prop1 ="",prop2 ="X_Y",prop3 ="Z_"},这将返回true.你可能不希望这样.
我不希望这个速度和我想要的一样快,不仅仅是对于值类型的拳击,还有"string.Format"的性能.我见过的另一个令人讨厌的是`new {prop1,prop2,prop3} .GetHashCode()`.不能评论哪一个在这两者之间会更慢.不要滥用工具.
是的,您总是可以用不常见的东西替换下划线符号(例如•,▲,►,◄,☺,☻),并希望您的用户不会使用这些符号...... :)

6> Ian Ringrose..:

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

    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;

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


从GetHashCode中抛出异常违反了Object合约.定义`GetHashCode`函数没有任何困难,使得任何两个相等的对象返回相同的哈希码; `return 24601;`和`return 8675309;`都是`GetHashCode`的有效实现."词典"的性能只有在项目数量较少时才会很好,如果项目数量变大会变得非常糟糕,但无论如何都会正常工作.
@RenniePet,由于抛出一个异常,最好还是因为执行无效而很难找到bug.
@supercat,如果对象中的标识字段可以更改,则无法以合理的方式实现GetHashCode,因为哈希代码必须永远不会更改.做你说的话可能导致某人不得不花费很多天来追踪性能问题,然后在大型系统上重新设计几周以取消使用字典.
我曾经为我定义的所有需要​​Equals()的类做了类似的事情,并且我完全确定我永远不会将该对象用作集合中的键.然后有一天,我使用像这样的对象的程序作为DevExpress XtraGrid控件的输入崩溃了.事实证明,在我的背后,XtraGrid正在创建一个基于我的对象的HashTable.我与DevExpress支持人们讨论了这个问题.我说他们将组件的功能和可靠性基于一个不明确的方法的未知客户实现是不明智的.

7> kemiller2002..:

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



8> BornToCode..:

只是添加以上答案:

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

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

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



9> Maciej..:

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



10> ILoveFortran..:

它不一定重要;它取决于集合的大小和性能要求,以及是否在可能不知道性能要求的库中使用您的类。我经常知道我的集合大小不是很大,并且我的时间比通过创建完美的哈希码获得几微秒的性能更有价值。因此(为了摆脱编译器的烦人警告),我只需使用:

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

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

当然,当您确实需要表现时,这里其他人提到的所有问题都适用。最重要的是 -否则从哈希集或字典中检索项目时,您会得到错误的结果:哈希码不得随对象的生存时间而变化(更准确地说,在需要哈希码的时间段内,例如字典中的键):例如,以下错误是由于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
      }
   }


不赞成投票。这是完全错误的。甚至Microsoft都在MSDN(http://msdn.microsoft.com/zh-cn/library/system.object.gethashcode.aspx)中声明,当对象状态以可能影响返回的方式更改时,GetHashCode的值必须更改调用Equals()的值,甚至在其示例中也显示了完全依赖可公开更改的值的GetHashCode实现。
塞巴斯蒂安(Sebastian),此外,在链接(http://msdn.microsoft.com/zh-cn/library/system.object.gethashcode.aspx)中看不到必须更改GetHashCode()的声明。相反,只要Equals为相同的参数返回相同的值,它就不得更改:“对象的GetHashCode方法必须一致地返回相同的哈希码,只要对确定返回值的对象状态没有任何修改即可。该语句的含义并不相反,如果Equals的返回值更改,则必须更改。
@Joao,您正在混淆合同的客户/消费者方与生产者/实施者。我说的是实现者的责任,该实现者重写GetHashCode()。您是在谈论消费者,即使用价值的消费者。
推荐阅读
ERIK又
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有