使用GetHashCode测试Equals覆盖中的相等性

时间:2010-11-22 18:50:52

标签: c# gethashcode

可以调用GetHashCode作为从Equals覆盖内部测试相等性的方法吗?

例如,此代码是否可以接受?

public class Class1
{
  public string A
  {
    get;
    set;
  }

  public string B
  {
    get;
    set;
  }

  public override bool Equals(object obj)
  {
    Class1 other = obj as Class1;
    return other != null && other.GetHashCode() == this.GetHashCode();
  }

  public override int GetHashCode()
  {
    int result = 0;
    result = (result ^ 397) ^ (A == null ? 0 : A.GetHashCode());
    result = (result ^ 397) ^ (B == null ? 0 : B.GetHashCode());
    return result;
  }
}

8 个答案:

答案 0 :(得分:14)

其他人是对的;你的平等操作被打破了。举例说明:

public static void Main()
{
    var c1 = new Class1() { A = "apahaa", B = null };
    var c2 = new Class1() { A = "abacaz", B = null };
    Console.WriteLine(c1.Equals(c2));
}

我想你想要那个程序的输出是“假的”,但是你对CLR的某些实现的相等定义是“真的”。

请记住,只有大约40亿个可能的哈希码。有超过40亿个可能的六个字母字符串,而因此至少有两个字母字符串具有相同的哈希码。我给你看了两个;还有更多的东西。

一般情况下,如果有n个可能的哈希码,那么一旦你了解了n个元素的平方根,那么获得冲突的几率会急剧上升。这就是所谓的“生日悖论”。关于为什么不应该依赖哈希码来实现相等性的文章,请参阅:

http://blogs.msdn.com/b/ericlippert/archive/2010/03/22/socks-birthdays-and-hash-collisions.aspx

答案 1 :(得分:7)

不,不行,因为不是

equality <=> hashcode equality

只是

equality => hashcode equality

或在另一个方向:

hashcode inequality => inequality

引用http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx

  
    

如果两个对象比较相等,则每个对象的GetHashCode方法必须返回相同的值。但是,如果两个对象的比较不相等,则两个对象的GetHashCode方法不必返回不同的值。

  

答案 2 :(得分:2)

我会说,除非您希望Equals基本上表示“与您的类型具有相同的哈希码”,然后,因为两个字符串可能不同但是共享相同的哈希码。概率可能很小,但不是零。

答案 3 :(得分:1)

这不是测试平等的可接受方式。 2个不相等的值很可能具有相同的哈希码。这会导致您Equals的实施在返回true时返回false

答案 4 :(得分:1)

您可以调用GetHashCode来确定项目是否相等,但如果两个对象返回相同的哈希码,则并不意味着它们 >平等。两个项目可以具有相同的哈希码但不相等。

如果比较两个项目的成本很高,那么您可以比较哈希码。如果他们不平等,那么你可以保释。否则(哈希码相等),你必须进行全面比较。

例如:

public override bool Equals(object obj)
  {
    Class1 other = obj as Class1;
    if (other == null || other.GetHashCode() != this.GetHashCode())
        return false;
    // the hash codes are the same so you have to do a full object compare.
  }

答案 5 :(得分:1)

不能说只是因为哈希码是相等的,那么对象必须是相等的。

GetHashCode内调用Equals的唯一一次是,如果为对象计算哈希值(比如因为缓存它)要比检查相等性要便宜得多。在这种情况下,您可以说if (this.GetHashCode() != other.GetHashCode()) return false;,以便您可以快速验证对象不相等。

所以你什么时候做到这一点?我写了一些代码,它定期截取屏幕截图,并试图找出屏幕更改后的时间。由于我的屏幕截图是8MB并且在屏幕截图间隔内相对较少的像素发生变化,因此搜索它们的列表以查找哪些相同是相当昂贵的。哈希值很小,每个屏幕截图只需要计算一次,这样就很容易消除已知的不相等的哈希值。实际上,在我的应用程序中,我认为具有相同的哈希值足够接近等于我甚至懒得实现Equals重载,导致C#编译器警告我我正在超载{{1}没有重载GetHashCode

答案 6 :(得分:0)

有一种情况是使用哈希码作为相等比较的捷径是有道理的。

考虑您正在构建哈希表或哈希集的情况。实际上,我们只考虑一下hashsets(哈希表通过保存一个值来扩展它,但这不相关)。

可以采用各种不同的方法,但是在所有这些方法中,您可以放置​​少量的散列值,我们采用开放或封闭的方式(这只是为了好玩,有些人使用与其他人相反的行话);如果我们在同一个插槽上碰撞两个不同的对象,我们可以将它们存储在同一个插槽中(但是有一个链接列表或实际存储对象的位置)或者通过重新探测来选择不同的插槽(有各种各样的插槽)对此的策略。

现在,无论使用哪种方法,我们都会使用散列表从O(1)复杂度转向O(n)复杂度。这种风险与可用的插槽数量成反比,因此在一定大小之后我们调整哈希表的大小(即使一切都很理想,如果存储的项目数大于数量,我们最终还是必须这样做时隙)。

重新插入调整大小的项目显然取决于哈希码。因此,虽然在一个对象中记忆GetHashCode()很少有意义(它在大多数对象上都没有经常被调用),但在哈希表本身(也许,或许,它)中记忆它确实是有意义的。记住生成的结果,例如,如果你使用Wang / Jenkins哈希重新散列,以减少由不良GetHashCode()实现造成的损害。

现在,当我们来插入时,我们的逻辑就像是:

  1. 获取对象的哈希码。
  2. 获取对象的插槽。
  3. 如果slot为空,请将对象放入其中并返回。
  4. 如果slot包含相同的对象,我们就完成了一个hashset,并且可以替换hashtable的值。这样做,然后返回。
  5. 根据碰撞策略尝试下一个插槽,并返回第3项(如果我们经常循环播放,可能需要调整大小)。
  6. 因此,在这种情况下,我们必须在比较相等性之前获取哈希码。我们还有已经预先计算的现有对象的哈希码,以允许调整大小。这两个事实的结合意味着将第4项的比较实施为:

    private bool IsMatch(KeyType newItem, KeyType storedItem, int newHash, int oldHash)
    {
      return ReferenceEquals(newItem, storedItem) // fast, false negatives, no false positives (only applicable to reference types)
        ||
        (
          newHash == oldHash // fast, false positives, no fast negatives
          &&
          _cmp.Equals(newItem, storedItem) // slow for some types, but always correct result.
        );
    }
    

    显然,这样做的好处取决于_cmp.Equals的复杂性。如果我们的密钥类型是int,那么这将是完全浪费。如果我们的键类型中的字符串和我们使用不区分大小写的Unicode规范化的相等比较(因此它甚至不能在长度上快捷方式)那么保存可能是值得的。

    通常,记忆哈希码没有意义,因为它们的使用频率不足以获得性能,但将它们存储在哈希集或哈希表本身中是有意义的。

答案 7 :(得分:0)

  1. 这是错误的实施,正如其他人所说的那样。

  2. 您应该使用GetHashCode短路检查相等性,如:

    if (other.GetHashCode() != this.GetHashCode()
        return false;
    

    Equals方法中,只有当您确定随后的Equals实施费用比GetHashCode 贵得多时,这绝不是绝大多数情况。

  3. 在你展示的这一个实现中(99%的情况)它不仅被破坏,而且还慢得多。原因呢? 计算你的属性的哈希几乎肯定比比较它们慢,所以你甚至没有获得性能方面的优势。实现正确GetHashCode的优点是,当您的类可以是哈希表的关键类型时,哈希只计算一次(并且该值用于比较)。在您的情况下,GetHashCode将被多次调用,如果它在一个集合中。尽管GetHashCode本身应该很快,但它并不比等效 Equals快。

    要进行基准测试,请运行Equals(正确的实现,取出当前基于哈希的实现)和GetHashCode此处

    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 100000; i++) 
    {
        action(); //Equals and GetHashCode called here to test for performance.
    }
    watch.Stop();
    Console.WriteLine(watch.Elapsed.TotalMilliseconds);