簡體   English   中英

為什么C#沒有為集合實現GetHashCode?

[英]Why does C# not implement GetHashCode for Collections?

我正在將一些東西從Java移植到C#。 在Java中, ArrayListhashcode取決於其中的項。 在C#中,我總是從List獲得相同的哈希碼...

為什么是這樣?

對於我的一些對象,哈希碼需要不同,因為列表屬性中的對象使對象不相等。 我希望哈希碼對於對象的狀態始終是唯一的,並且當對象相等時僅等於另一個哈希碼。 我錯了嗎?

為了正常工作,哈希碼必須是不可變的 - 對象的哈希碼必須永遠不會改變。

如果對象的哈希碼確實發生了變化,那么包含該對象的任何詞典都將停止工作。

由於集合不是不可變的,因此它們無法實現GetHashCode
相反,它們繼承了默認的GetHashCode ,它為對象的每個實例返回(希望)唯一值。 (通常基於內存地址)

是的,你錯了。 在Java和C#中,相等意味着具有相同的哈希碼,但反過來並不(必然)為真。

有關更多信息,請參閱GetHashCode

Hashcodes必須依賴於所使用的相等的定義,這樣如果A == BA.GetHashCode() == B.GetHashCode() (但不一定是逆; A.GetHashCode() == B.GetHashCode()不需要A == B )。

默認情況下,值類型的等式定義基於其值,而引用類型的等式定義基於其標識(即,默認情況下,引用類型的實例僅等於其自身),因此默認的哈希碼為值類型是這樣的,它取決於它包含的字段的值*,對於引用類型,它取決於標識。 實際上,因為我們理想地希望非等對象的哈希碼特別是在低階位(最有可能影響重新散列的值)中不同,我們通常希望兩個等價但不相等的對象具有不同的哈希值。

由於對象將保持與自身相等,因此即使對象發生變異(即使對於可變對象,身份也不會發生變異GetHashCode() ,也應該清楚GetHashCode()默認實現將繼續具有相同的值。

現在,在某些情況下,引用類型(或值類型)重新定義相等性。 一個例子是字符串,例如"ABC" == "AB" + "C" 雖然比較了兩個不同的字符串實例,但它們被認為是相同的。 在這種情況下,必須重寫GetHashCode()以便該值與定義相等性的狀態(在本例中為包含的字符序列)相關。

雖然使用也是不可變的類型更常見,但由於各種原因, GetHashCode()不依賴於不變性 相反, GetHashCode()必須在可變性面前保持一致 - 更改我們在確定哈希時使用的值,並且哈希必須相應地更改。 但請注意,如果我們使用這個可變對象作為使用哈希的結構的鍵,這是一個問題,因為改變對象會改變它應該存儲的位置,而不會將其移動到該位置(它也是如此)任何其他情況,其中集合中對象的位置取決於其值 - 例如,如果我們對列表進行排序然后改變列表中的一個項目,則不再對列表進行排序)。 但是,這並不意味着我們必須只在字典和散列集中使用不可變對象。 相反,它意味着我們不能改變這種結構中的對象,並使其不可變是一種明確的方法來保證這一點。

實際上,有很多情況下需要在這種結構中存儲可變對象,並且只要我們在此期間不改變它們,這就沒問題了。 由於我們沒有不可變性帶來的保證,因此我們希望以另一種方式提供它(例如在集合中花費很短的時間並且只能從一個線程訪問)。

因此,關鍵值的不變性是可能的事情之一,但通常是一個想法。 但是,對於定義哈希碼算法的人來說,並不是他們認為任何這樣的情況總是一個壞主意(他們甚至不知道在對象存儲在這樣的結構中時發生了變異); 它們是為了實現在對象的當前狀態上定義的哈希碼,無論是否在給定點調用它都是好的。 因此,例如,除非在每個mutate上清除memoisation,否則不應在可變對象上記憶哈希碼。 (無論如何,記憶哈希通常都是浪費,因為反復敲擊相同對象哈希碼的結構會有自己的備忘錄)。

現在,在手頭的情況下,ArrayList在基於身份的默認情況下進行操作,例如:

ArrayList a = new ArrayList();
ArrayList b = new ArrayList();
for(int i = 0; i != 10; ++i)
{
  a.Add(i);
  b.Add(i);
}
return a == b;//returns false

現在,這實際上是一件好事。 為什么? 那么,你怎么知道在上面我們要考慮a等於b? 我們可能,但在其他情況下也有很多充分理由不這樣做。

更重要的是,從基於身份到基於價值的重新定義平等要容易得多,而不是從基於價值的轉變為基於身份的平等。 最后,對於許多對象,有多個基於值的相等定義(經典案例是關於什么使字符串相等的不同視圖),因此甚至沒有一個唯一的定義可行。 例如:

ArrayList c = new ArrayList();
for(short i = 0; i != 10; ++i)
{
  c.Add(i);
}

如果我們考慮上面a == b ,我們應該考慮a == c aslo嗎? 答案取決於我們所使用的平等定義中我們關心的內容,因此框架無法知道所有案例的正確答案是什么,因為所有案例都不同意。

現在,如果我們在特定情況下關注基於價值的平等,我們有兩個非常簡單的選擇。 第一個是子類化和覆蓋平等:

public class ValueEqualList : ArrayList, IEquatable<ValueEqualList>
{
  /*.. most methods left out ..*/
  public Equals(ValueEqualList other)//optional but a good idea almost always when we redefine equality
  {
    if(other == null)
      return false;
    if(ReferenceEquals(this, other))//identity still entails equality, so this is a good shortcut
      return true;
    if(Count != other.Count)
      return false;
    for(int i = 0; i != Count; ++i)
      if(this[i] != other[i])
        return false;
    return true;
  }
  public override bool Equals(object other)
  {
    return Equals(other as ValueEqualList);
  }
  public override int GetHashCode()
  {
    int res = 0x2D2816FE;
    foreach(var item in this)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

這假設我們總是希望以這種方式處理這樣的列表。 我們還可以為給定的案例實現IEqualityComparer:

public class ArrayListEqComp : IEqualityComparer<ArrayList>
{//we might also implement the non-generic IEqualityComparer, omitted for brevity
  public bool Equals(ArrayList x, ArrayList y)
  {
    if(ReferenceEquals(x, y))
      return true;
    if(x == null || y == null || x.Count != y.Count)
      return false;
    for(int i = 0; i != x.Count; ++i)
      if(x[i] != y[i])
        return false;
    return true;
  }
  public int GetHashCode(ArrayList obj)
  {
    int res = 0x2D2816FE;
    foreach(var item in obj)
    {
        res = res * 31 + (item == null ? 0 : item.GetHashCode());
    }
    return res;
  }
}

綜上所述:

  1. 引用類型的默認相等定義僅取決於標識。
  2. 大多數時候,我們都想要那樣。
  3. 當定義類的人決定這不是想要的時,他們可以覆蓋這種行為。
  4. 當使用該類的人再次想要不同的相等定義時,他們可以使用IEqualityComparer<T>IEqualityComparer因此他們的字典,哈希映射,哈希集等使用它們的相等概念。
  5. 改變對象是一個災難性的,而它是基於散列的結構的關鍵。 可以使用不變性來確保不會發生這種情況,但不是強制性的,也不總是可取的。

總而言之,該框架為我們提供了很好的默認值和詳細的覆蓋可能性。

*在結構中有一個小數的情況下有一個錯誤,因為在某些情況下使用快捷方式時它是安全的而不是其他的,但是當包含小數的結構是短時間的一個結構時切割是不安全的,它被錯誤地識別為安全的情況。

哈希碼不可能在大多數非平凡類的所有變體中都是唯一的。 在C#中,List相等的概念與Java中的概念不同(參見此處 ),因此哈希代碼實現也不相同 - 它反映了C#List的相等性。

性能和人性的核心原因 - 人們傾向於將哈希視為快速的東西,但通常需要至少遍歷一次對象的所有元素。

示例:如果您使用字符串作為哈希表中的鍵,則每個查詢都具有復雜度O(| s |) - 使用2x更長的字符串,它將花費您至少兩倍的費用。 想象一下,它是一個完整的樹(只是一個列表) - 哎呀:-)

如果完整的,深度哈希計算是對集合的標准操作,那么很大比例的程序員會在不知情的情況下使用它,然后將框架和虛擬機歸咎於緩慢。 對於像完全遍歷一樣昂貴的東西,程序員必須意識到復雜性是至關重要的。 唯一要實現的就是確保你必須自己編寫。 這也是一個很好的威懾:-)

另一個原因是更新策略 每次計算和更新散列與每次完整計算需要根據手頭的具體情況進行判斷調用。

Immutabilty只是一個學術警察 - 人們將哈希作為一種更快地檢測變化的方式(例如文件哈希),並且還使用哈希來處理一直在變化的復雜結構。 Hash在101個基礎知識中有更多用途。 關鍵在於,對於復雜對象的散列使用什么必須是逐個判斷調用。

使用對象的地址(實際上是一個句柄,因此它不會在GC之后更改)作為哈希實際上是哈希值對於任意可變對象保持相同的情況:-) C#的原因是它便宜並再次推動人們自己計算。

你只是部分錯了。 當您認為相等的哈希碼意味着相等的對象時,你肯定是錯的,但是相等的對象必須具有相同的哈希碼,這意味着如果哈希碼不同,那么對象也是如此。

為什么太哲學了。 創建輔助方法(可能是擴展方法)並根據需要計算哈希碼。 可能是XOR元素的哈希碼

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM