关于C#:当equals方法被重写时,为什么重写gethashcode很重要?

Why is it important to override GetHashCode when Equals method is overridden?

给出了以下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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很重要?


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

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

  • 如果两个事物相等(Equals(...) == true,则它们必须返回GetHashCode()的相同值。
  • 如果GetHashCode()相等,它们就不必相同;这是一次碰撞,将调用Equals来查看它是否真正相等。

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

1
2
3
4
5
6
7
8
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时提供==!=操作符。

当你犯了这个错误时会发生什么的演示就在这里。


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

当我和NHibernate一起工作时,我终于找到了解决这个问题的方法。我的方法是根据对象的ID计算散列代码。只能通过构造函数设置ID,因此,如果要更改ID(这是不太可能的),则必须创建一个新对象,该对象具有新的ID,因此需要创建一个新的哈希代码。这种方法最适合于guid,因为您可以提供一个无参数的构造函数,它随机生成一个ID。


通过重写equals,您基本上是在声明您是更了解如何比较给定类型的两个实例的人,因此您可能是提供最佳哈希代码的最佳候选。

以下是Resharper如何为您编写getHashCode()函数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
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;
    }
}

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


覆盖Equals()时,请不要忘记对照null检查obj参数。并比较类型。

1
2
3
4
5
6
7
8
9
public override bool Equals(object obj)
{
    if (obj == null || GetType() != obj.GetType())
        return false;

    Foo fooItem = obj as Foo;

    return fooItem.FooId == this.FooId;
}

原因是:与null相比,Equals必须返回false。另请参阅http://msdn.microsoft.com/en-us/library/bsc2ak47.aspx


怎么样:

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

Assuming performance is not an issue :)


只需添加以上答案:

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

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

此外,除了在字典中使用对象时可能找不到的问题(因为它是由一个哈希代码插入的,当您查找对象时,默认的哈希代码可能会有所不同,并且会再次调用equals(),就像Marc Gravell在他的回答中解释的那样,您还引入了对不允许相同键的字典或哈希集概念-您已经声明,当override equals时,这些对象本质上是相同的,因此您不希望它们都作为数据结构上的不同键(假定具有唯一键)。但由于它们具有不同的哈希代码,"相同"键将作为不同的键插入。


我们有两个问题要处理。

  • 如果在对象可以更改。通常情况下,对象不会在依赖于GetHashCode()的集合。所以成本实施GetHashCode()通常不值得,或者不值得可能的。

  • 如果有人将对象放入调用你已经凌驾于Equals()之上,而没有GetHashCode()行为端正,该人可能会花费数天时间。跟踪问题。

  • 因此,默认情况下我会这样做。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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");
        }
    }


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


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


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

    1
    2
    3
    4
       public override int GetHashCode()
       {
          return base.GetHashCode();
       }

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

    当你处在一个你确实需要表现的位置时,当然这里其他人提到的所有问题都适用。最重要的是-否则,当从哈希集或字典中检索项时,您将得到错误的结果:哈希代码不能随对象的生命周期而变化(更准确地说,在需要哈希代码的时间内,例如当哈希代码是字典中的键时):例如,由于值是公共的,因此以下是错误的,因此可以是C在实例的生命周期内挂起到类的外部,因此不能将其用作哈希代码的基础:

    1
    2
    3
    4
    5
    6
    7
    8
    9
       class A
       {
          public int Value;

          public override int GetHashCode()
          {
             return Value.GetHashCode(); //WRONG! Value is not constant during the instance's life time
          }
       }

    另一方面,如果值不能更改,可以使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
       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
          }
       }


    据我所知,原始gethashcode()返回对象的内存地址,因此,如果要比较两个不同的对象,则必须重写它。

    编辑:这是不正确的,原始的getHashCode()方法无法确保两个值相等。尽管相同的对象返回相同的哈希代码。


    在我看来,下面使用反射是考虑公共属性的一个更好的选择,因为这样您就不必担心属性的添加/删除(尽管不是很常见的场景)。我发现这也表现得更好。(与使用诊断秒表的时间相比)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
        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;  
        }