致力于一個(gè)應(yīng)該避免編寫(xiě)的方法。GetHashCode()僅僅用在一個(gè)地方:在基于hash(哈希)結(jié)構(gòu)的集合中,用來(lái)定義key(鍵值)的hash值,典型的是Hashtable(哈希表)或者Dictionary(字典)容器。因?yàn)榛?lèi)在對(duì)GetHashCode()的實(shí)現(xiàn)上存在很多問(wèn)題,所以?xún)H用在一個(gè)地方很好。對(duì)于引用類(lèi)型,這也能工作但是效率低。對(duì)于值類(lèi)型,基類(lèi)的版本經(jīng)常是不正確的,而且越來(lái)越糟。不寫(xiě)GetHashCode()是完全可能的,那樣就會(huì)同時(shí)獲得效率和正確性。沒(méi)有哪個(gè)單獨(dú)的方法比GetHashCode()帶來(lái)更多的討論和混亂。繼續(xù)讀來(lái)移除所有的困惑。
如果你正在定義一個(gè)從不會(huì)在容器里面用作key的類(lèi)型,這沒(méi)什么影響。表示W(wǎng)inForm控件、web頁(yè)面控件或數(shù)據(jù)庫(kù)連接的類(lèi)型,不大可能被用作集合中的key。在那些情況下,什么也不要做。所有的引用類(lèi)型將會(huì)有一個(gè)正確的hash碼,即使是很低效的。值類(lèi)型應(yīng)該是不可變性的,這種情況下,默認(rèn)的實(shí)現(xiàn),盡管是效率低的,但是是可以工作的。在你創(chuàng)建的多數(shù)類(lèi)型中,的途徑就是完全避免GetHashCode()的存在。
Examda提示:創(chuàng)建一個(gè)要用作hashtable的key的類(lèi)型,需要編寫(xiě)自己的GetHashCode()實(shí)現(xiàn),那么繼續(xù)讀。基于hash結(jié)構(gòu)的容器使用hash碼來(lái)優(yōu)化搜索。每個(gè)對(duì)象生成一個(gè)叫做hash碼的整型值。對(duì)象都被存儲(chǔ)在基于hash值的bucket(容器,桶?)里。為了搜索一個(gè)對(duì)象,你需要它的鍵值,在bucket容器里面搜索它。在.Net里面,每個(gè)對(duì)象都有一個(gè)由System.Object.GetHashCode()決定的hash碼。任何對(duì)GetHashCode()的重載必須遵守這三個(gè)規(guī)則:
1.如果2個(gè)對(duì)象是相等的(由==操作符定義)它們必須生成同樣的hash值。否則,hash值不能被用來(lái)在容器里面查找對(duì)象。
2.對(duì)于任何對(duì)象A,A.GetHashCode()必須是一個(gè)實(shí)例不變量。無(wú)論在A里面調(diào)用什么方法,A.GetHashCode()必須總是返回同樣的值。這能保證,放在bucket容器里的對(duì)象永遠(yuǎn)在正確的bucket里。
3.Hash方法應(yīng)該為所有的輸入在整型范圍內(nèi)生成一個(gè)隨機(jī)的分布。這就是使用基于hash結(jié)構(gòu)的容器里面獲得效率的原因。
編寫(xiě)一個(gè)正確且高效的hash方法要求對(duì)該類(lèi)型有更多了解來(lái)保證遵守規(guī)則3。在System.Object和System.ValueType中定義的版本沒(méi)有這優(yōu)點(diǎn)。這些版本在幾乎不知道你的特定類(lèi)型的情況下,必須提供的默認(rèn)行為。Object.GetHashCode()使用了System.Object類(lèi)的一個(gè)內(nèi)部字段來(lái)生成hash值。每個(gè)對(duì)象在它被創(chuàng)建的時(shí)候都被分配一個(gè)的對(duì)象值(以一個(gè)整型值來(lái)存儲(chǔ))。這些值以1開(kāi)始,每次有任何類(lèi)型的一個(gè)新對(duì)象被創(chuàng)建時(shí)該值就會(huì)增加。對(duì)象標(biāo)識(shí)符字段在System.Object構(gòu)造器的內(nèi)部被設(shè)置,以后不能再被修改。Object.GetHashCode()將對(duì)象標(biāo)識(shí)符字段的hash值作為結(jié)果hash值返回。
現(xiàn)在根據(jù)那三條規(guī)則來(lái)檢查Object.GetHashCode()。如果2個(gè)對(duì)象是相等的,除非你重寫(xiě)過(guò)了==操作符,Object.GetHashCode()會(huì)返回同樣的hash值。System.Object的==版本檢測(cè)對(duì)象標(biāo)識(shí)符。GetHashCode()返回內(nèi)部的對(duì)象標(biāo)識(shí)符字段,這能工作。然而,如果你已經(jīng)提供了自己版本的==,就必須也要提供自己版本的GetHashCode()才能確保遵守了第一條規(guī)則。Item 9詳細(xì)介紹了相等性。
遵循了第二個(gè)規(guī)則:一個(gè)對(duì)象在被創(chuàng)建后,hash碼從不改變。
第三個(gè)規(guī)則,對(duì)所有的輸入要隨機(jī)分布在整型范圍內(nèi),這一條不成立。除非你創(chuàng)建大量的對(duì)象,否則一個(gè)數(shù)字隊(duì)列不是整型范圍內(nèi)的隨機(jī)分布,由Object.GetHashCode()生成的hash碼集中在整型范圍的低端部分。
這意味著Object.GetHashCode()是正確的但是非高效的。如果你創(chuàng)建一個(gè)基于你定義的引用類(lèi)型的hashtable,繼承自System.Object的默認(rèn)行為就是可工作、比較慢的hashtable。當(dāng)你創(chuàng)建一個(gè)準(zhǔn)備作為hash鍵值的引用類(lèi)型時(shí),應(yīng)該重寫(xiě)GetHashCode(),以便于為你的特定類(lèi)型在整型范圍內(nèi)得到一個(gè)更好的hash值分布。
在講述怎么編寫(xiě)自己重寫(xiě)版本的GetHashCode之前,這一節(jié)用那三條同樣的規(guī)則來(lái)檢查Value.GetHashCode()。System.ValueType重寫(xiě)了GetHashCode(),為所有的值類(lèi)型提供了默認(rèn)的行為。這個(gè)版本返回在該類(lèi)型內(nèi)部定義的首個(gè)字段的hash值作為自己的hash值??紤]這個(gè)例子:
public struct MyStruct
{
private String msg;
private Int32 id;
private DateTime epoch;
}
從MyStruct對(duì)象返回的hash碼就是由msg字段生成的hash碼。下面代碼段總是返回true:
MyStruct s = new MyStruct();
return s.GetHashCode() == s.msg.GetHashCode();
翻譯時(shí),環(huán)境.Net2.0,試驗(yàn):
總是返回false
第一個(gè)規(guī)則是說(shuō)2個(gè)相等的對(duì)象(由==定義的相等)必須由相同的hash碼。該規(guī)則對(duì)于值類(lèi)型來(lái)說(shuō),在多數(shù)情況下是被遵守的。但是你可以打破它,就像對(duì)待引用類(lèi)型一樣。ValueType的操作符==()比較結(jié)構(gòu)體中很多字段中的首個(gè)字段,這滿(mǎn)足了規(guī)則1。只要你定義了任何重寫(xiě)的==操作符,就使用了首個(gè)字段,就能工作。任何結(jié)構(gòu)體,如果它的首個(gè)字段沒(méi)有參與類(lèi)型的相等性,那么就違背了該規(guī)則,破壞了GetHashCode()。
第二個(gè)規(guī)則闡明了hash碼必須是一個(gè)實(shí)例不變量。只有當(dāng)這個(gè)結(jié)構(gòu)體中的首個(gè)字段是不可變字段時(shí),才符合該規(guī)則。如果首個(gè)字段的值可改變,那么hash碼也可變,這就違背了該規(guī)則。是的,對(duì)于任何你創(chuàng)建的結(jié)構(gòu)體,如果在它的生命期內(nèi)首個(gè)字段是可以被修改的,那么GetHashCode()就會(huì)被打破。為什么不可變的值類(lèi)型是你的選擇呢,這也是另外一個(gè)原因(參看Item 17)。
第三個(gè)規(guī)則依賴(lài)于首個(gè)字段的類(lèi)型和它如何被使用。如果首個(gè)字段生成了一個(gè)在整型范圍的隨機(jī)分布,而且它也遍布了結(jié)構(gòu)中的所有值,那么,該結(jié)體構(gòu)也能生成一個(gè)很好的平均分布。然而,如果首個(gè)字段經(jīng)常有同樣的值,這個(gè)規(guī)則也會(huì)被打破??紤]對(duì)前面的結(jié)構(gòu)體做個(gè)小小的修改;
public struct MyStruct
{
private DateTime epoch;
private String msg;
private Int32 id;
}
如果epoch字段被設(shè)置成了當(dāng)前的日期(不含時(shí)間),所有在某個(gè)特定日期被創(chuàng)建的MyStruct對(duì)象將會(huì)有同樣的hash值。這就阻止了所有hash值的平均分布。
概括Object.GetHashCode()的默認(rèn)行為,在引用類(lèi)型上工作得很正確,盡管它沒(méi)必要生成一個(gè)高效的分布(如果你已經(jīng)重寫(xiě)了Object.operator==(),會(huì)打破GetHashCode())。只有在結(jié)構(gòu)體中的首個(gè)字段是只讀的情況下,ValueType.GetHashCode()才能工作。只有當(dāng)結(jié)構(gòu)體滿(mǎn)足下列條件的時(shí)候:包含了遍布于他的輸入中某個(gè)有意義的集合的值,ValueType.GetHashCode()才能生成高效的hash碼, 如果你正打算構(gòu)建一個(gè)更好的hash碼,需要在你的類(lèi)型里面加入一些限制。重新檢測(cè)這三個(gè)規(guī)則,這次是在構(gòu)建一個(gè)可工作的對(duì)GetHashCode()的實(shí)現(xiàn)的上下文中來(lái)檢測(cè)?!∈紫?,如果2個(gè)對(duì)象是==操作符定義的相等的話,它們必須返回同樣的hash值,任何被用來(lái)生成hash碼的屬性或者數(shù)據(jù)值必須參加該類(lèi)型的相等性判斷。顯然,這意味著,被用作相等性的屬性同時(shí)也被用作來(lái)生成hash碼。有的屬性參與相等性判斷,但不被用來(lái)進(jìn)行hash碼計(jì)算,這也是可能的。System.ValueType的默認(rèn)行為就是那樣做的,但是這意味著規(guī)則3經(jīng)常被違背,同樣的數(shù)據(jù)元素應(yīng)該同時(shí)參加2個(gè)計(jì)算。
第二條規(guī)則是,GetHashCode()返回的值必須是一個(gè)實(shí)不例變量。想象,你定義了一個(gè)引用類(lèi)型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 Int32 GetHashCode()
{
return name.GetHashCode();
}
}
假設(shè)執(zhí)行下面的代碼段:
Customer c1 = new Customer("Acme Products");
myHashMap.Add(c1, orders);
// Oops, the name is wrong:
c1.Name = "Acme Software";
C1遺失在hashmap(hash圖)中的某個(gè)地方。當(dāng)你把c1放到圖中時(shí),hash碼由字符串“Acme Products”生成。在將客戶(hù)的名字修改為“Acme Software”之后,hash碼值發(fā)生了變化。現(xiàn)在它由新的名字“Acme Software”生成。C1存儲(chǔ)在以“Acme Products”定義的bucket容器里面,但是它應(yīng)該存儲(chǔ)在以“Acme Software”定義的bucket容器里面。你已經(jīng)將客戶(hù)遺失在了自己的集合里面,因?yàn)閔ash碼不是一個(gè)對(duì)象不變量。在存儲(chǔ)完該對(duì)象之后,你已經(jīng)修改了這個(gè)正確的bucket容器。
僅僅當(dāng)Customer是一個(gè)引用類(lèi)型時(shí),前面的情況才會(huì)發(fā)生。值類(lèi)型做了不同的錯(cuò)誤行為,但是它們也會(huì)引起問(wèn)題。如果Customer是值類(lèi)型,c1的一個(gè)拷貝就會(huì)被存儲(chǔ)在hash圖中。最后一行修改name值的代碼對(duì)存儲(chǔ)在hash圖中的拷貝沒(méi)有影響。因?yàn)檠b箱和拆箱都是進(jìn)行拷貝的,所以,在一個(gè)值類(lèi)型的對(duì)象被添加到一個(gè)集合中后,想修改它的成員是非常不可能的。
表達(dá)規(guī)則2的方法就是定義hash碼方法,讓它返回一個(gè)基于一個(gè)或多個(gè)不變屬性的值。System.Object通過(guò)使用不變的對(duì)象標(biāo)識(shí)符遵守了該規(guī)則。System.ValueType希望你的類(lèi)型的首個(gè)字段不會(huì)改變。除了使你的類(lèi)型是個(gè)不可變的之外,沒(méi)有更好的方法。當(dāng)你定義一個(gè)準(zhǔn)備在hash容器中作為key使用的值類(lèi)型時(shí),它必須是一個(gè)不可變的類(lèi)型。若違背該建議,你的類(lèi)型的用戶(hù)將會(huì)找到一個(gè)打破將你的類(lèi)型用作key的hashtable的方法。再看Customer類(lèi),你可以修改使用戶(hù)名不可變:
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 Int32 GetHashCode()
{
return name.GetHashCode();
}
}
使name不可變,將改變你的下述行為:你該如何處理客戶(hù)對(duì)象來(lái)修改name:
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);
你不得不移除原始的客戶(hù),修改name,將新的客戶(hù)對(duì)象添加到hashtable中。它看起來(lái)比第一個(gè)版本更笨重,但能工作。前面的版本允許程序員編寫(xiě)不正確的代碼。通過(guò)將被用來(lái)計(jì)算hash值的屬性強(qiáng)制為不可變的,可以得到正確的行為,你的類(lèi)型的用戶(hù)不會(huì)出錯(cuò)了。是的,這個(gè)版本更能工作。你正在強(qiáng)迫開(kāi)發(fā)者編寫(xiě)更多的代碼,但是僅僅因?yàn)檫@是編寫(xiě)正確代碼的方式。請(qǐng)確認(rèn)任何被用來(lái)計(jì)算hash值的數(shù)據(jù)成員是不可變的。
第三個(gè)規(guī)則是說(shuō)GetHashCode()應(yīng)該為所有的輸入生成一個(gè)在整型范圍內(nèi)的隨機(jī)分布。要滿(mǎn)足這個(gè)要求依賴(lài)于你創(chuàng)建的類(lèi)型的細(xì)節(jié)。如果存在一個(gè)魔法公式,就肯定早就在System.Object里面實(shí)現(xiàn)了,而這個(gè)條款也不會(huì)存在。一個(gè)通用并且成功的算法是:對(duì)類(lèi)型里面的所有字段使用GetHashCode()后,對(duì)其返回值取XOR。如果你的類(lèi)型包含一些可變的字段,在計(jì)算中排除它們。
GetHashCode()有非常特別的要求:相等的對(duì)象必須產(chǎn)生相等的hash碼,hash碼必須是對(duì)象不可變的,必須產(chǎn)生一個(gè)平均的分布以便獲得效率。僅僅有不可變的值類(lèi)型才能滿(mǎn)足3個(gè)規(guī)則,對(duì)于其它類(lèi)型,依賴(lài)于默認(rèn)的行為,但是要理解它的缺陷。
如果你正在定義一個(gè)從不會(huì)在容器里面用作key的類(lèi)型,這沒(méi)什么影響。表示W(wǎng)inForm控件、web頁(yè)面控件或數(shù)據(jù)庫(kù)連接的類(lèi)型,不大可能被用作集合中的key。在那些情況下,什么也不要做。所有的引用類(lèi)型將會(huì)有一個(gè)正確的hash碼,即使是很低效的。值類(lèi)型應(yīng)該是不可變性的,這種情況下,默認(rèn)的實(shí)現(xiàn),盡管是效率低的,但是是可以工作的。在你創(chuàng)建的多數(shù)類(lèi)型中,的途徑就是完全避免GetHashCode()的存在。
Examda提示:創(chuàng)建一個(gè)要用作hashtable的key的類(lèi)型,需要編寫(xiě)自己的GetHashCode()實(shí)現(xiàn),那么繼續(xù)讀。基于hash結(jié)構(gòu)的容器使用hash碼來(lái)優(yōu)化搜索。每個(gè)對(duì)象生成一個(gè)叫做hash碼的整型值。對(duì)象都被存儲(chǔ)在基于hash值的bucket(容器,桶?)里。為了搜索一個(gè)對(duì)象,你需要它的鍵值,在bucket容器里面搜索它。在.Net里面,每個(gè)對(duì)象都有一個(gè)由System.Object.GetHashCode()決定的hash碼。任何對(duì)GetHashCode()的重載必須遵守這三個(gè)規(guī)則:
1.如果2個(gè)對(duì)象是相等的(由==操作符定義)它們必須生成同樣的hash值。否則,hash值不能被用來(lái)在容器里面查找對(duì)象。
2.對(duì)于任何對(duì)象A,A.GetHashCode()必須是一個(gè)實(shí)例不變量。無(wú)論在A里面調(diào)用什么方法,A.GetHashCode()必須總是返回同樣的值。這能保證,放在bucket容器里的對(duì)象永遠(yuǎn)在正確的bucket里。
3.Hash方法應(yīng)該為所有的輸入在整型范圍內(nèi)生成一個(gè)隨機(jī)的分布。這就是使用基于hash結(jié)構(gòu)的容器里面獲得效率的原因。
編寫(xiě)一個(gè)正確且高效的hash方法要求對(duì)該類(lèi)型有更多了解來(lái)保證遵守規(guī)則3。在System.Object和System.ValueType中定義的版本沒(méi)有這優(yōu)點(diǎn)。這些版本在幾乎不知道你的特定類(lèi)型的情況下,必須提供的默認(rèn)行為。Object.GetHashCode()使用了System.Object類(lèi)的一個(gè)內(nèi)部字段來(lái)生成hash值。每個(gè)對(duì)象在它被創(chuàng)建的時(shí)候都被分配一個(gè)的對(duì)象值(以一個(gè)整型值來(lái)存儲(chǔ))。這些值以1開(kāi)始,每次有任何類(lèi)型的一個(gè)新對(duì)象被創(chuàng)建時(shí)該值就會(huì)增加。對(duì)象標(biāo)識(shí)符字段在System.Object構(gòu)造器的內(nèi)部被設(shè)置,以后不能再被修改。Object.GetHashCode()將對(duì)象標(biāo)識(shí)符字段的hash值作為結(jié)果hash值返回。
現(xiàn)在根據(jù)那三條規(guī)則來(lái)檢查Object.GetHashCode()。如果2個(gè)對(duì)象是相等的,除非你重寫(xiě)過(guò)了==操作符,Object.GetHashCode()會(huì)返回同樣的hash值。System.Object的==版本檢測(cè)對(duì)象標(biāo)識(shí)符。GetHashCode()返回內(nèi)部的對(duì)象標(biāo)識(shí)符字段,這能工作。然而,如果你已經(jīng)提供了自己版本的==,就必須也要提供自己版本的GetHashCode()才能確保遵守了第一條規(guī)則。Item 9詳細(xì)介紹了相等性。
遵循了第二個(gè)規(guī)則:一個(gè)對(duì)象在被創(chuàng)建后,hash碼從不改變。
第三個(gè)規(guī)則,對(duì)所有的輸入要隨機(jī)分布在整型范圍內(nèi),這一條不成立。除非你創(chuàng)建大量的對(duì)象,否則一個(gè)數(shù)字隊(duì)列不是整型范圍內(nèi)的隨機(jī)分布,由Object.GetHashCode()生成的hash碼集中在整型范圍的低端部分。
這意味著Object.GetHashCode()是正確的但是非高效的。如果你創(chuàng)建一個(gè)基于你定義的引用類(lèi)型的hashtable,繼承自System.Object的默認(rèn)行為就是可工作、比較慢的hashtable。當(dāng)你創(chuàng)建一個(gè)準(zhǔn)備作為hash鍵值的引用類(lèi)型時(shí),應(yīng)該重寫(xiě)GetHashCode(),以便于為你的特定類(lèi)型在整型范圍內(nèi)得到一個(gè)更好的hash值分布。
在講述怎么編寫(xiě)自己重寫(xiě)版本的GetHashCode之前,這一節(jié)用那三條同樣的規(guī)則來(lái)檢查Value.GetHashCode()。System.ValueType重寫(xiě)了GetHashCode(),為所有的值類(lèi)型提供了默認(rèn)的行為。這個(gè)版本返回在該類(lèi)型內(nèi)部定義的首個(gè)字段的hash值作為自己的hash值??紤]這個(gè)例子:
public struct MyStruct
{
private String msg;
private Int32 id;
private DateTime epoch;
}
從MyStruct對(duì)象返回的hash碼就是由msg字段生成的hash碼。下面代碼段總是返回true:
MyStruct s = new MyStruct();
return s.GetHashCode() == s.msg.GetHashCode();
翻譯時(shí),環(huán)境.Net2.0,試驗(yàn):
總是返回false
第一個(gè)規(guī)則是說(shuō)2個(gè)相等的對(duì)象(由==定義的相等)必須由相同的hash碼。該規(guī)則對(duì)于值類(lèi)型來(lái)說(shuō),在多數(shù)情況下是被遵守的。但是你可以打破它,就像對(duì)待引用類(lèi)型一樣。ValueType的操作符==()比較結(jié)構(gòu)體中很多字段中的首個(gè)字段,這滿(mǎn)足了規(guī)則1。只要你定義了任何重寫(xiě)的==操作符,就使用了首個(gè)字段,就能工作。任何結(jié)構(gòu)體,如果它的首個(gè)字段沒(méi)有參與類(lèi)型的相等性,那么就違背了該規(guī)則,破壞了GetHashCode()。
第二個(gè)規(guī)則闡明了hash碼必須是一個(gè)實(shí)例不變量。只有當(dāng)這個(gè)結(jié)構(gòu)體中的首個(gè)字段是不可變字段時(shí),才符合該規(guī)則。如果首個(gè)字段的值可改變,那么hash碼也可變,這就違背了該規(guī)則。是的,對(duì)于任何你創(chuàng)建的結(jié)構(gòu)體,如果在它的生命期內(nèi)首個(gè)字段是可以被修改的,那么GetHashCode()就會(huì)被打破。為什么不可變的值類(lèi)型是你的選擇呢,這也是另外一個(gè)原因(參看Item 17)。
第三個(gè)規(guī)則依賴(lài)于首個(gè)字段的類(lèi)型和它如何被使用。如果首個(gè)字段生成了一個(gè)在整型范圍的隨機(jī)分布,而且它也遍布了結(jié)構(gòu)中的所有值,那么,該結(jié)體構(gòu)也能生成一個(gè)很好的平均分布。然而,如果首個(gè)字段經(jīng)常有同樣的值,這個(gè)規(guī)則也會(huì)被打破??紤]對(duì)前面的結(jié)構(gòu)體做個(gè)小小的修改;
public struct MyStruct
{
private DateTime epoch;
private String msg;
private Int32 id;
}
如果epoch字段被設(shè)置成了當(dāng)前的日期(不含時(shí)間),所有在某個(gè)特定日期被創(chuàng)建的MyStruct對(duì)象將會(huì)有同樣的hash值。這就阻止了所有hash值的平均分布。
概括Object.GetHashCode()的默認(rèn)行為,在引用類(lèi)型上工作得很正確,盡管它沒(méi)必要生成一個(gè)高效的分布(如果你已經(jīng)重寫(xiě)了Object.operator==(),會(huì)打破GetHashCode())。只有在結(jié)構(gòu)體中的首個(gè)字段是只讀的情況下,ValueType.GetHashCode()才能工作。只有當(dāng)結(jié)構(gòu)體滿(mǎn)足下列條件的時(shí)候:包含了遍布于他的輸入中某個(gè)有意義的集合的值,ValueType.GetHashCode()才能生成高效的hash碼, 如果你正打算構(gòu)建一個(gè)更好的hash碼,需要在你的類(lèi)型里面加入一些限制。重新檢測(cè)這三個(gè)規(guī)則,這次是在構(gòu)建一個(gè)可工作的對(duì)GetHashCode()的實(shí)現(xiàn)的上下文中來(lái)檢測(cè)?!∈紫?,如果2個(gè)對(duì)象是==操作符定義的相等的話,它們必須返回同樣的hash值,任何被用來(lái)生成hash碼的屬性或者數(shù)據(jù)值必須參加該類(lèi)型的相等性判斷。顯然,這意味著,被用作相等性的屬性同時(shí)也被用作來(lái)生成hash碼。有的屬性參與相等性判斷,但不被用來(lái)進(jìn)行hash碼計(jì)算,這也是可能的。System.ValueType的默認(rèn)行為就是那樣做的,但是這意味著規(guī)則3經(jīng)常被違背,同樣的數(shù)據(jù)元素應(yīng)該同時(shí)參加2個(gè)計(jì)算。
第二條規(guī)則是,GetHashCode()返回的值必須是一個(gè)實(shí)不例變量。想象,你定義了一個(gè)引用類(lèi)型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 Int32 GetHashCode()
{
return name.GetHashCode();
}
}
假設(shè)執(zhí)行下面的代碼段:
Customer c1 = new Customer("Acme Products");
myHashMap.Add(c1, orders);
// Oops, the name is wrong:
c1.Name = "Acme Software";
C1遺失在hashmap(hash圖)中的某個(gè)地方。當(dāng)你把c1放到圖中時(shí),hash碼由字符串“Acme Products”生成。在將客戶(hù)的名字修改為“Acme Software”之后,hash碼值發(fā)生了變化。現(xiàn)在它由新的名字“Acme Software”生成。C1存儲(chǔ)在以“Acme Products”定義的bucket容器里面,但是它應(yīng)該存儲(chǔ)在以“Acme Software”定義的bucket容器里面。你已經(jīng)將客戶(hù)遺失在了自己的集合里面,因?yàn)閔ash碼不是一個(gè)對(duì)象不變量。在存儲(chǔ)完該對(duì)象之后,你已經(jīng)修改了這個(gè)正確的bucket容器。
僅僅當(dāng)Customer是一個(gè)引用類(lèi)型時(shí),前面的情況才會(huì)發(fā)生。值類(lèi)型做了不同的錯(cuò)誤行為,但是它們也會(huì)引起問(wèn)題。如果Customer是值類(lèi)型,c1的一個(gè)拷貝就會(huì)被存儲(chǔ)在hash圖中。最后一行修改name值的代碼對(duì)存儲(chǔ)在hash圖中的拷貝沒(méi)有影響。因?yàn)檠b箱和拆箱都是進(jìn)行拷貝的,所以,在一個(gè)值類(lèi)型的對(duì)象被添加到一個(gè)集合中后,想修改它的成員是非常不可能的。
表達(dá)規(guī)則2的方法就是定義hash碼方法,讓它返回一個(gè)基于一個(gè)或多個(gè)不變屬性的值。System.Object通過(guò)使用不變的對(duì)象標(biāo)識(shí)符遵守了該規(guī)則。System.ValueType希望你的類(lèi)型的首個(gè)字段不會(huì)改變。除了使你的類(lèi)型是個(gè)不可變的之外,沒(méi)有更好的方法。當(dāng)你定義一個(gè)準(zhǔn)備在hash容器中作為key使用的值類(lèi)型時(shí),它必須是一個(gè)不可變的類(lèi)型。若違背該建議,你的類(lèi)型的用戶(hù)將會(huì)找到一個(gè)打破將你的類(lèi)型用作key的hashtable的方法。再看Customer類(lèi),你可以修改使用戶(hù)名不可變:
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 Int32 GetHashCode()
{
return name.GetHashCode();
}
}
使name不可變,將改變你的下述行為:你該如何處理客戶(hù)對(duì)象來(lái)修改name:
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);
你不得不移除原始的客戶(hù),修改name,將新的客戶(hù)對(duì)象添加到hashtable中。它看起來(lái)比第一個(gè)版本更笨重,但能工作。前面的版本允許程序員編寫(xiě)不正確的代碼。通過(guò)將被用來(lái)計(jì)算hash值的屬性強(qiáng)制為不可變的,可以得到正確的行為,你的類(lèi)型的用戶(hù)不會(huì)出錯(cuò)了。是的,這個(gè)版本更能工作。你正在強(qiáng)迫開(kāi)發(fā)者編寫(xiě)更多的代碼,但是僅僅因?yàn)檫@是編寫(xiě)正確代碼的方式。請(qǐng)確認(rèn)任何被用來(lái)計(jì)算hash值的數(shù)據(jù)成員是不可變的。
第三個(gè)規(guī)則是說(shuō)GetHashCode()應(yīng)該為所有的輸入生成一個(gè)在整型范圍內(nèi)的隨機(jī)分布。要滿(mǎn)足這個(gè)要求依賴(lài)于你創(chuàng)建的類(lèi)型的細(xì)節(jié)。如果存在一個(gè)魔法公式,就肯定早就在System.Object里面實(shí)現(xiàn)了,而這個(gè)條款也不會(huì)存在。一個(gè)通用并且成功的算法是:對(duì)類(lèi)型里面的所有字段使用GetHashCode()后,對(duì)其返回值取XOR。如果你的類(lèi)型包含一些可變的字段,在計(jì)算中排除它們。
GetHashCode()有非常特別的要求:相等的對(duì)象必須產(chǎn)生相等的hash碼,hash碼必須是對(duì)象不可變的,必須產(chǎn)生一個(gè)平均的分布以便獲得效率。僅僅有不可變的值類(lèi)型才能滿(mǎn)足3個(gè)規(guī)則,對(duì)于其它類(lèi)型,依賴(lài)于默認(rèn)的行為,但是要理解它的缺陷。