程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則10:明白GetHashCode()的缺陷

Effective C#原則10:明白GetHashCode()的缺陷

編輯:關於C#

這是本書中唯一一個被一整個函數占用的原則,你應該避免寫這樣的函數。 GetHashCode()僅在一種情況下使用:那就是對象被用於基於散列的集合的關鍵 詞,如經典的HashTable或者Dictionary容器。這很不錯,由於在基類上實現的 GetHashCode()存在大量的問題。對於引用類型,它可以工作,但高效不高;對 於值類型,基類的實現經常出錯。這更糟糕。你自己完全可以寫一個即高效又正 確的GetHashCode()。沒有那個單一的函數比GetHashCode()討論的更多,且令人 困惑。往下看,為你解釋困惑。

如果你定義了一個類型,而且你決不准 備把它用於某個容器的關鍵詞,那就沒什麼事了。像窗體控件,網頁控件,或者 數據庫鏈接這樣的類型是不怎像要做為某個任何的關鍵詞的。在這些情況下,什 麼都不用做了。所有的引用類型都會得到一個正確的散列值,即使這樣效率很糟 糕。值類型應該是恆定的(參見原則7),這種情況下,默認的實現總是工作的, 盡管這樣的效率也是很糟糕的。在大多數情況下,你最好完全避免在類型的實例 上使用GetHashCode()。

然而,在某天你創建了一個要做為HashTable的 關鍵詞來使用的類型,那麼你就須要重寫你自己的GetHashCode()的實現了。繼 續看,基於散列(算法)的集合用散列值來優化查找。每一個對象產生一個整型的 散列值,而該對象就存儲在基於這個散列值的“桶”中。為了查找某 個對象,你通過它的散列值來找到這個(存儲了實際對象的)“桶”。 在.Net裡,每一對象都有一個散列值,它是由System.Object.GetHashCode()斷 定的。任何對GetHashCode()的重寫都必須遵守下面的三個規則:

1、如 果兩個對象是相等的(由操作符==所定義),那麼它們必須產生相同的散列值。否 則,無法通過散列值在容器中找到對象。

2、對於任意對象A, A.GetHashCode()必須是實例不變的。不管在A上調用了什麼方法, A.GetHashCode()必須總是返回同樣的散列值。這就保證在某個“桶 ”中的對象始終是在這個“桶”中。

3、對於任意的輸 入,散列函數總是產生產生一個介於整型內的隨機分布。這會讓你在一個基於散 列的容器取得好的效率。

為一個類型寫一個正確且高效的散列函數來滿 足上面的三條,要對該類型有廣泛的認識。System.Object和System.ValueType 的默認版本並不具備什麼優勢。這些版本必須為你的特殊類型提供默認的行為, 而同時它們對這些特殊的類型又並不了解。Object.GetHashCode()是使用 System.Object內在的成員來產生散列值。每個對象在產生時指定一個唯一的值 來做為對象關鍵詞,(這個值)以整型來存儲。這些關鍵詞從1開始,在每次有任 何新的對象產生時逐漸增加。對象的ID字段在System.Object的構造函數進行設 置,並且今後再也不能修改。Object.GetHashCode()就是把這個值當成給定對象 的散列值來返回。

(譯注:注意這裡說的是默認的引用類型,其它情況就 不是這樣的了。)

現在我們根據上面的三個原則來驗證 Object.GetHashCode()。如果兩個對象是相等的,Object.GetHashCode()返回同 樣的散列值,除非你重寫了操作符==。System.Object這個版本的==操作符是檢 測對象的ID。GetHashCode()返回對象內部的ID字段,它是好的。然而,如果你 提供了自己的==版本,你就必須同時提供你自己版本的GetHashCode(),從而保 證遵守了前面說的第一條規則。相等的細節參見原則9。

第二條規則已經 遵守了:一個對象創建後,它的散列值是不能改變的。

第三條規則,對 所有的輸入,在整型內進行隨機分布,這並沒有被支持。這個數字序列並不是整 型上的隨機分布,除非你創建了大量的對象。Object.GetHashCode()所產生的散 列值主要集中在盡可能小的整型范圍內。

這就是說這個 Object.GetHashCode()是正確的,但並不高效。如果你在你定義的引用類型上創 建一個散列表,這個默認從System.Object上繼承的行為是工作的,但是比較慢 。當你創建准備用於散列關鍵詞的引用類型時,你應該為你的特殊類型重寫 GetHashCode(),從而提供更好的在整型范圍上隨機分布的散列值。

在講 解如何重寫你自己的GetHashCode()之前,這一節來驗證 ValueType.GetHashCode()是否也遵守上面的三條規則。System.ValueType重寫 了GetHashCode(),為所有的值類型提供默認的行為。這一版本從你所定義的類 型的第一個字段上返回散列。考慮這個例子:

public struct MyStruct
{
 private string  _msg;
 private int    _id;
 private DateTime _epoch;
}

從MyStruct對 象上返回的散列值是從該對象的_msg成員上生成的。下面的代碼片斷總是返回 true:

MyStruct s = new MyStruct( );
return s.GetHashCode( ) == s._msg.GetHashCode( );

規則1表示,兩 個相等的對象(用操作符==定義的)必須返回相同的散列值。這一規則被大多數值 類型遵守著,但你可以破壞它, just as you could with for reference types. ValueType的操作符==()與其它成員一起來比較結構的第一個字段。這是 滿足第一條規則的。只要在任何時候你所重寫的==操作符用第一個字段,這將可 以正常工作。任何不以第一個字段斷定相等的結構將違反這一規則,從而破壞 GetHashCode().

第二條規則表示,散列值必須是實例不變的。這一規則 只有當結構的第一個成員字段是恆定類型時才被遵守。如果第一個字段的值發生 了改變,那麼散列值也會發生改變。這就破壞了這規則。是的,如果你的結構的 實例對象在它的生存期內改變了結構的第一個字段,那麼GetHashCode()就破壞 了這一規則。這也就是另一個原因,你最好的原則就是把值類型設置為恆定類型 (參見原則7)。

第三個規則依懶於第一個字段以及是如何使用它的。如果 你的第一個字段能保證產生一個在整型范圍上的隨機分布,並且第一個字段的分 布能復蓋結構的所有其它值,那麼這個結構就很好的保證了一個均衡的分布(譯 注:就是說結構的第一個字段可以唯一的決定一個實例)。然而,如果第一個字 段經常具有相同的值,那麼這一規則也會被破壞。考慮對前面的結構做一個小的 修改:

public struct MyStruct
{
 private DateTime _epoch;
 private string  _msg;
 private int    _id;
}

如果_epoch字段設置的是當前日期(不包含時間) ,所有在同一給定日期裡創建的對象具有相同的散列值。這防礙了在所有散列值 中進行均衡的分布。

概括一個默認的行為,Object.GetHashCode()可以 正確的在引用類型上工作,盡管它不是必須保證一個高效的分布。(如果你有一 個對Object的==操作符的重載,你會破壞GetHashCode())。 ValueType.GetHashCode()僅在你的結構的第一個字段是只讀的時候才能正確工 作。而當你的結構的第一個字段的值,復蓋了它所能接受的輸入的有意義的子集 時,ValueType.GetHashCode()就可以保證一個高效的散列值。

如果你准 備創建一個更好的散列值,你須要為你的類型建立一些約束。再次驗證上面的三 條規則,現在我們來實現一個可工作的GetHashCode()。

首先,如果兩個 對象相等,就是由操作符==所定義的,它們必須返回同樣的散列值。類型的任何 承擔散列值的屬性或者數據值也必須參與相等比較。顯然,這就意味著同樣用於 相等比較的的屬性也用於散列值的生成。然而很可能所有與相等比較的屬性,並 不用於散列值的計算。System.ValueType的默認行為就是這樣的,也就是說它經 常違反規則3。同樣的數據元素應該參同時參與兩個運算(比較和散列)。

第二條規則就是GetHashCode()返回的值必須是實例不變的。想象你已經了一個 引用類型,Customer:

public class Customer
{
  private string _name;
 private decimal _revenue;
 public Customer( string name )
 {
  _name = name;
 }
 public string Name
 {
  get { return _name; }
   set { _name = value; }
 }
 public override int GetHashCode()
 {
  return _name.GetHashCode();
 }
}

假設你運行下面的代碼片斷:

Customer c1 = new Customer( "Acme Products" );
myHashMap.Add( c1, orders );
// Oops, the name is wrong:
c1.Name = "Acme Software";

c1在某些地方會丟失散列映射。當 你把c1放在映射中時,散列值是由字符串“Acme Products”來保證 的。當你把客戶的名字修改為“Acme Software”後,散列值也發生 了改變。現在是由新的名字:“Acme Software”來保證的了。c1存 儲在一個由“Acme Products”決定的“桶”內,而它不 應該存在於由“Acme Software”決定的“桶”內。你將 會在你自己的集合中丟失這個客戶。丟失的原因就是散列值不是實例不變的。你 在對象存儲後,改變了正確的“桶”。

前面的情形只有當 Customer是引用類型時才出現。而當是值類型時,會有不同的錯誤行為,而且同 樣是帶來麻煩的。如果Customer是一個值類型,c1的一個拷貝會存在在散列映射 中。因為裝箱與拆箱很會拷貝數據。這並不是很可靠,當你在把一個值類型對象 添加到集合之後,你還可以修改值類型的成員數據。

唯一安置好規則2的 方法就是,定義一個散列函數,它依懶於對象的一些不變的屬性來返回散列值。 System.Object是通過不變的對象ID來遵守這一規則的。System.ValueType希望 你的類型的第一個字段不發生改變。除了把你的類型設計為恆定類型以外,你沒 有更好的方法了。當你准備定義一個在散列容器中當關鍵詞使用的值類型時,它 必須是一個恆定的類型。如果不遵守勸告,你的用戶就可以把你的類型做為一個 關鍵詞在散列表中使用,進而找到一個方法破壞散列表。 更正Customer類,你 可以修改它,使客戶名成為一個恆定的:

public class Customer
{
 private readonly string _name;
 private decimal _revenue;
 public Customer( string name ) :
   this ( name, 0 )
 {
 }
 public Customer( string name, decimal revenue )
 {
  _name = name;
   _revenue = revenue;
 }
 public string Name
 {
  get { return _name; }
 }
 // Change the name, returning a new object:
 public Customer ChangeName( string newName )
 {
  return new Customer( newName, _revenue );
 }
 public override int GetHashCode()
 {
   return _name.GetHashCode();
 }
}

使名字成為 恆定的類型後,你要怎樣才能修改一個客戶對象的名字呢:

Customer c1 = new Customer( "Acme Products" );
myHashMap.Add( c1,orders );
// Oops, the name is wrong:
Customer c2 = c1.ChangeName( "Acme Software" );
Order o = myHashMap[ c1 ] as Order;
myHashMap.Remove( c1 );
myHashMap.Add( c2, o );

你已經移除了原來的客戶, 修改了名字,然後添加一個新的客戶對象到散列表中。這看上去比原來的更麻煩 ,但它可以正確工作。先前的版本充許程序員寫一些不正確的代碼。通過強制使 用恆定屬性來生成散列值後,你就增加了正確的行為。你的用戶就不會出錯了。 是的,這個版本可以更好的工作。你迫使開發人員寫更多的代碼,但這是因為只 有這樣才能寫正確的代碼。請確保參與散列運算的數據成員是恆定的。

第三條規則是說,GetHashCode()應該對所有的輸入隨機的生成一個分布在整型 范圍內的值。這一需求依懶於你實際創建的類型。如果有一個神奇的公式存在, 那它應該在System.Object中實現了,並且這一原則(譯注:這裡說的是全文這一 原則)也將不存在了。一個通用而且成功的算法就是XOR(異或)運算,對一個類型 內的所有字段的散列再進行異或後返回。如果你的類型裡包含一些持久字段,計 算時應該排除它們。

GetHashCode()具有很特殊的要求:相等的對象必須 產生相等的散列值,並且散列值必須是對象不變的,並且是均衡的高效分布。所 有這些只有對恆定類型才能滿足(譯注:本文前面已經說過了,.Net框架中的 System.Object.GetHashCode()其實並不滿足均衡高效分布這一規則)。對於其它 類型,就交給默認的行為吧,知道它的缺點就行了。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved