簡體   English   中英

C#中的GetHashCode指南

[英]GetHashCode Guidelines in C#

我在Essential C#3.0和.NET 3.5書中讀到:

GetHashCode()在特定對象的生命周期內的返回應該是常量(相同的值),即使對象的數據發生了變化。 在許多情況下,您應該緩存方法返回以強制執行此操作。

這是一個有效的指導方針嗎?

我在.NET中嘗試了幾種內置類型,但它們的行為並不像這樣。

已經很長一段時間了,但我認為仍然有必要對這個問題給出正確的答案,包括對這些問題的解釋。 到目前為止,最好的答案是引用MSDN的人 - 不要試圖制定自己的規則,MS人員知道他們在做什么。

但首先要做的是:問題中引用的指南是錯誤的。

現在是為什么 - 有兩個

第一個原因 :如果哈希碼是以某種方式計算的,那么它在對象的生命周期內不會改變,即使對象本身發生了變化,也不會破壞等於契約。

請記住:“如果兩個對象比較相等,則每個對象的GetHashCode方法必須返回相同的值。但是,如果兩個對象的比較不相等,則兩個對象的GetHashCode方法不必返回不同的值。”

第二句經常被誤解為“唯一的規則是,在對象創建時,相等對象的哈希碼必須相等”。 不知道為什么,但這也是大多數答案的本質。

想想兩個包含名稱的對象,其名稱在equals方法中使用:相同名稱 - >相同的東西。 創建實例A:Name = Joe創建實例B:Name = Peter

Hashcode A和Hashcode B很可能不一樣。 當實例B的名稱更改為Joe時,現在會發生什么?

根據問題的指導原則,B的哈希碼不會改變。 結果如下:A.Equals(B)==> true但同時:A.GetHashCode()== B.GetHashCode()==> false。

但正是這種行為被equals&hashcode-contract明確禁止。

第二個原因 :雖然它當然是 - 但是,哈希碼中的更改可能會使用哈希碼破壞散列列表和其他對象,反之亦然。 在最壞的情況下,不更改哈希碼將獲得散列列表,其中所有許多不同的對象將具有相同的哈希碼,因此在相同的哈希箱中 - 例如,當使用標准值初始化對象時發生。


現在來看看嗯,乍一看,似乎有一個矛盾 - 無論哪種方式,代碼都會破裂。 但這兩個問題都不是來自更改或未更改的哈希碼。

問題的根源在MSDN中有詳細描述:

從MSDN的哈希表條目:

密鑰對象必須是不可變的,只要它們在Hashtable中用作密鑰即可。

這意味着:

當對象發生變化時,任何創建哈希值的對象都應該更改哈希值,但是當它在Hashtable(或任何其他使用Hash的對象)中使用時,它不能 - 絕對不能 - 允許對自身進行任何更改。

首先,最簡單的方法當然是設計僅用於哈希表的不可變對象,這些對象將在需要時創建為普通對象的可復制對象。 在不可變對象內部,緩存哈希碼顯然是可以的,因為它是不可變的。

第二個如何或者給對象一個“你現在被哈希”-flag,確保所有對象數據都是私有的,檢查所有可以改變對象數據的函數中的標志,如果不允許更改則拋出異常數據(即標志設置) )。 現在,當您將對象放在任何散列區域時,請確保設置標志,並且 - 以及 - 在不再需要時取消設置標志。 為了便於使用,我建議在“GetHashCode”方法中自動設置標志 - 這樣就不會忘記。 並且顯式調用“ResetHashFlag”方法將確保程序員必須思考,現在是否允許更改對象數據。

好吧,還應該說些什么:有些情況下,當對象數據發生變化時,可以使對象具有可變數據,其中哈希碼仍未改變,而不違反equals&hashcode-contract。

然而,這確實需要equals方法也不基於可變數據。 所以,如果我編寫一個對象,並創建一個GetHashCode方法,它只計算一次值並將其存儲在對象中以便在以后的調用中返回它,那么我必須再次:絕對必須創建一個將使用的Equals方法存儲的比較值,以便A.Equals(B)永遠不會從false變為true。 否則,合同將被打破。 這樣做的結果通常是Equals方法沒有任何意義 - 它不是原始引用等於,但它既不是值等於。 有時,這可能是預期的行為(即客戶記錄),但通常不是。

因此,當對象數據發生更改時,只需更改GetHashCode結果,並且如果使用列表或對象在哈希內部使用對象(或者只是可能),則使對象不可變或創建只讀標志以用於包含該對象的散列列表的生命周期。

(順便說一句:所有這些都不是特定於C#oder的.NET - 它具有所有散列表實現的性質,或者更常見的是任何索引列表的性質,標識對象的數據永遠不會改變,而對象在列表中如果此規則被破壞,將會發生意外和不可預測的行為。在某些地方,可能存在列表實現,它會監視列表中的所有元素並自動重新索引列表 - 但這些的性能肯定會令人毛骨悚然。)

答案主要是,它是一個有效的指導方針,但可能不是一個有效的規則。 它也沒有講述整個故事。

要點是,對於可變類型,您不能將哈希代碼基於可變數據,因為兩個相等的對象必須返回相同的哈希代碼,並且哈希代碼必須在對象的生命周期內有效。 如果哈希代碼發生更改,您最終會得到一個在哈希集合中丟失的對象,因為它不再存在於正確的哈希箱中。

例如,對象A返回哈希值1.因此,它進入哈希表的bin 1。 然后你改變對象A使得它返回2的散列。當散列表尋找它時,它在bin 2中查找並且找不到它 - 該對象在bin 1中是孤立的。這就是散列碼必須的原因。不會改變 對象的生命周期 ,只是編寫GetHashCode實現的一個原因是對接的痛苦。

更新
Eric Lippert發布​​了一個博客 ,提供有關GetHashCode優秀信息。

其他更新
我上面做了幾處修改:

  1. 我對指南和規則進行了區分。
  2. 我突破了“對象的一生”。

指南只是一個指南,而不是一個規則。 實際上,當事物期望對象遵循指南時, GetHashCode只需要遵循這些准則,例如當它存儲在哈希表中時。 如果您從未打算在哈希表中使用您的對象(或任何依賴於GetHashCode規則的其他對象),那么您的實現不需要遵循這些准則。

當您看到“對於對象的生命周期”時,您應該閱讀“對象需要與哈希表協作的時間”或類似內容。 像大多數事情一樣, GetHashCode就是知道何時違反規則。

來自MSDN

如果兩個對象比較相等,則每個對象的GetHashCode方法必須返回相同的值。 但是,如果兩個對象的比較不相等,則兩個對象的GetHashCode方法不必返回不同的值。

只要沒有對對象狀態的修改來確定對象的Equals方法的返回值,對象的GetHashCode方法必須始終返回相同的哈希代碼。 請注意,這僅適用於當前應用程序的執行,並且如果再次運行應用程序,則可以返回不同的哈希代碼。

為獲得最佳性能,哈希函數必須為所有輸入生成隨機分布。

這意味着如果對象的值發生變化,則哈希碼應該更改。 例如,將“Name”屬性設置為“Tom”的“Person”類應該有一個哈希碼,如果將名稱更改為“Jerry”,則應該使用不同的代碼。 否則,湯姆==傑里,這可能不是你想要的。


編輯

也來自MSDN:

覆蓋GetHashCode的派生類也必須重寫Equals以保證兩個被認為相等的對象具有相同的哈希碼; 否則,Hashtable類型可能無法正常工作。

MSDN的哈希表條目

密鑰對象必須是不可變的,只要它們在Hashtable中用作密鑰即可。

我讀這個的方式是,可變對象應該在它們的值發生變化時返回不同的哈希碼, 除非它們被設計用於哈希表。

在System.Drawing.Point的示例中,對象是可變的,並且返回不同的散列碼,當X或Y的值的變化。 這將使它成為在哈希表中原樣使用的不良候選者。

我認為有關GetHashcode的文檔有點令人困惑。

一方面,MSDN聲明對象的哈希碼永遠不會改變,並且是常量。另一方面,MSDN還聲明GetHashcode的返回值對於2個對象應該相等,如果這兩個對象被認為是相等的。

MSDN:

哈希函數必須具有以下屬性:

  • 如果兩個對象比較相等,則每個對象的GetHashCode方法必須返回相同的值。 但是,如果兩個對象的比較不相等,則兩個對象的GetHashCode方法不必返回不同的值。
  • 只要沒有對對象狀態的修改來確定對象的Equals方法的返回值,對象的GetHashCode方法必須始終返回相同的哈希代碼。 請注意,這僅適用於當前應用程序的執行,並且如果再次運行應用程序,則可以返回不同的哈希代碼。
  • 為獲得最佳性能,哈希函數必須為所有輸入生成隨機分布。

然后,這意味着所有對象都應該是不可變的,或者GetHashcode方法應該基於對象的不可變屬性。 假設你有這個類(天真的實現):

public class SomeThing
{
      public string Name {get; set;}

      public override GetHashCode()
      {
          return Name.GetHashcode();
      }

      public override Equals(object other)
      {
           SomeThing = other as Something;
           if( other == null ) return false;
           return this.Name == other.Name;
      }
}

此實現已違反可在MSDN中找到的規則。 假設你有這個類的2個實例; instance1的Name屬性設置為'Pol',instance2的Name屬性設置為'Piet'。 兩個實例都返回不同的哈希碼,它們也不相等。 現在,假設我將instance2的名稱更改為'Pol',然后,根據我的Equals方法,兩個實例應該相等,並且根據MSDN的一個規則,它們應該返回相同的哈希碼。
但是,這不能完成,因為instance2的哈希碼將改變,並且MSDN聲明不允許這樣做。

然后,如果您有一個實體,您可以實現哈希碼,以便它使用該實體的“主要標識符”,這可能是理想的代理鍵或不可變屬性。 如果您有一個值對象,則可以實現Hashcode,以便它使用該值對象的“屬性”。 這些屬性構成了值對象的“定義”。 這當然是價值對象的本質; 你不是對它的身份感興趣,而是對它的價值感興趣。
因此,值對象應該是不可變的。 (就像它們在.NET框架中一樣,字符串,日期等......都是不可變對象)。

另一件事是:
在'session'期間(​​我真的不知道應該如何調用它)應該'GetHashCode'返回一個常量值。 假設您打開應用程序,從DB(實體)中加載對象的實例,並獲取其哈希碼。 它會返回一定數量。 關閉應用程序,然后加載相同的實體。 是否要求此次哈希碼具有與第一次加載實體時相同的值? 恕我直言,不是。

這是個好建議。 以下是Brian Pepin就此事所說的話:

這使我不止一次:確保GetHashCode始終在實例的生命周期內返回相同的值。 請記住,哈希碼用於在大多數哈希表實現中標識“桶”。 如果對象的“存儲桶”發生更改,則哈希表可能無法找到您的對象。 這些可能是非常難以找到的錯誤,所以第一次就把它弄好。

看看Marc Brooks的這篇博客文章:

VTO,RTO和GetHashCode() - 哦,我的!

然后查看后續帖子(不能鏈接,因為我是新的,但是在initlal文章中有一個鏈接),它進一步討論並涵蓋了初始實現中的一些小缺點。

這是我需要知道的關於創建GetHashCode()實現的所有內容,他甚至提供了他的方法以及其他一些實用程序的下載,簡言之。

不是直接回答你的問題,但是 - 如果你使用Resharper,不要忘記它有一個為你生成合理的GetHashCode實現(以及Equals方法)的功能。 您當然可以指定在計算哈希碼時將考慮該類的哪些成員。

哈希碼永遠不會改變,但了解哈希碼的來源也很重要。

如果您的對象使用值語義,即對象的標識由其值定義(如String,Color,所有結構)。 如果對象的標識獨立於其所有值,則Hashcode由其值的子集標識。 例如,您的StackOverflow條目存儲在某個數據庫中。 如果您更改了您的姓名或電子郵件,您的客戶條目將保持不變,盡管某些值已更改(最終您通常會通過一些長客戶ID識別#)。

簡而言之:

值類型語義 - Hashcode由值定義引用類型語義 - Hashcode由某個id定義

我建議你閱讀埃里克埃文斯的領域驅動設計,他進入實體與價值類型(這或多或少是我上面嘗試做的),如果這仍然沒有意義。

查看Eric Lippert的GetHashCode指南和規則

暫無
暫無

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

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