繁体   English   中英

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

[英]Using GetHashCode to test equality in Equals override

是否可以调用 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;
  }
}

其他人是对的; 你的平等操作被打破了。 为了显示:

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

不,这不行,因为它不是

equality <=> hashcode equality

只是

equality => hashcode equality

或在另一个方向:

hashcode inequality => inequality

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

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

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

不,这不是测试相等性的可接受方式。 2 个不相等的值很可能具有相同的哈希码。 这将导致您的Equals true在应该返回false时返回false

你可以调用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.
  }

不能仅仅因为哈希码相等就说对象必须相等。

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

那么你什么时候会这样做呢? 我编写了一些代码,它定期截取屏幕截图,并尝试找出屏幕更改后的时间。 由于我的屏幕截图大小为 8MB,并且在屏幕截图间隔内变化的像素相对较少,因此搜索它们的列表以查找相同的像素是相当昂贵的。 哈希值很小,每个屏幕截图只需计算一次,可以轻松消除已知的不相等值。 事实上,在我的应用程序中,我认为具有相同的哈希值足够接近相等,以至于我什至没有费心实现Equals重载,导致 C# 编译器警告我我正在重载GetHashCode而不重载Equals

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

在一种情况下,使用哈希码作为等式比较的捷径是有意义的。

考虑您正在构建哈希表或哈希集的情况。 事实上,让我们只考虑散列集(散列表通过还持有一个值来扩展它,但这并不相关)。

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

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

在调整大小时重新插入项目显然取决于哈希码。 正因为如此,虽然在对象中记住GetHashCode()很少有意义(它只是在大多数对象上调用得不够频繁),但在哈希表本身中记住它肯定是有意义的(或者也许,记住生成的结果,例如如果您使用 Wang/Jenkins 哈希重新哈希以减少由错误的GetHashCode()实现造成的损害。

现在,当我们开始插入时,我们的逻辑将是这样的:

  1. 获取对象的哈希码。
  2. 获取对象的插槽。
  3. 如果插槽为空,则将对象放入其中并返回。
  4. 如果 slot 包含相等的对象,我们就完成了一个 hashset 并且有位置替换一个 hashtable 的值。 这样做,然后返回。
  5. 根据碰撞策略尝试下一个插槽,并返回到第 3 项(如果我们经常循环,可能会调整大小)。

因此,在这种情况下,我们必须在比较相等性之前获取哈希码。 我们也已经预先计算了现有对象的哈希码以允许调整大小。 这两个事实的结合意味着对第 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那么这将完全是一种浪费。 如果我们的键类型 where string 和我们使用不区分大小写的 Unicode 规范化相等比较(因此它甚至不能缩短长度),那么节省很值得。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM