簡體   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