建議12: 重寫Equals時也要重寫GetHashCode
除非考慮到自定義類型會被用作基于散列的集合的鍵值;否則,不建議重寫Equals方法,因為這會帶來一系列的問題。
如果編譯上一個建議中的Person這個類型,編譯器會提示這樣一個信息:
“重寫 Object.Equals(object o)但不重寫 Object.GetHashCode()”
如果重寫Equals方法的時候不重寫GetHashCode方法,在使用如FCL中的Dictionary類時,可能隱含一些潛在的Bug。還是針對上一個建議中的Person進行編碼,代碼如下所示:
static Dictionary<Person, PersonMoreInfo> PersonValues = new Dictionary<Person, PersonMoreInfo>(); static void Main(string[] args) { AddAPerson(); Person mike = new Person("NB123"); //Console.WriteLine(mike.GetHashCode()); Console.WriteLine(PersonValues.ContainsKey(mike)); } static void AddAPerson() { Person mike = new Person("NB123"); PersonMoreInfo mikeValue = new PersonMoreInfo() { SomeInfo = "Mike's info" }; PersonValues.Add(mike, mikeValue); //Console.WriteLine(mike.GetHashCode()); Console.WriteLine(PersonValues.ContainsKey(mike)); }
本段代碼的輸出將會是:
True
False
理論上來說,在上一個建議中我們已經重寫了Person的Equals方法;也就是說,在AddAPerson方法中的mike和Main方法中的 mike屬于“值相等”。于是,將該“值”作為key放入Dictionary中,再在某處根據mike將mikeValue取出來,這會是理所當然的事 情。可是,從上面的代碼段中我們發現,針對同一個示例,這種結論是正確的,若是針對不同的mike示例,這種結果就有問題了。
基于鍵值的集合(如上面的Dictionary)會根據Key值來查找Value值。CLR內部會優化這種查找,實際上,最終是根據Key值的 HashCode來查找Value值。代碼運行的時候,CLR首先會調用Person類型的GetHashCode,由于發現Person沒有實現 GetHashCode,所以CLR最終會調用Object的GetHashCode方法。將上面代碼中的兩行注釋代碼去掉,運行程序得到輸出,我們會發 現,Main方法和AddAPerson方法中的兩個mike的HashCode是不同的。這里需要解釋為什么兩者實際對應調用的 Object.GetHashCode會不相同。
Object為所有的CLR類型都提供了GetHashCode的默認實現。每new一個對象,CLR都會為該對象生成一個固定的整型值,該整型值 在對象的生存周期內不會改變,而該對象默認的GetHashCode實現就是對該整型值求HashCode。所以,在上面代碼中,兩個mike對象雖然屬 性值都一致,但是它們默認實現的HashCode不一致,這就導致Dictionary中出現異常的行為。若要修正該問題,就必須重寫 GetHashCode方法。Person類的一個簡單的重寫可以是如下的形式:
public override int GetHashCode() { return this.IDCode.GetHashCode(); }
此時再運行本條建議開始時代碼的輸出,就會發現兩者的HashCode是一致的,而Dictionary也會找到相應的鍵值,輸出:True。
細心的讀者可能已經發現一個問題,Person類的IDCode屬性是一個只讀屬性。從語法特性本身來講,可以將IDCode設置為可寫;然而從現 實的角度考慮,一個“人”一旦踏入社會,其IDCode就不應該改變,如果要改變,就相當于是另外一個人了。所以,我們應該只實現該IDCode的只讀屬 性。同理,GetHashCode方法也應該基于那些只讀的屬性或特性生成HashCode。
GetHashCode方法還存在另外一個問題,它永遠只返回一個整型類型,而整型類型的容量顯然無法滿足字符串的容量,以下的例子就能產生兩個同樣的HashCode。
string str1 = "NB0903100006"; string str2 = "NB0904140001"; Console.WriteLine(str1.GetHashCode()); Console.WriteLine(str2.GetHashCode());
為了減少兩個不同類型之間根據字符串產生相同的HashCode的幾率,一個稍作改進版本的GetHashCode方法如下:
public override int GetHashCode() { return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode(); }
注意 重寫Equals方法的同時,也應該實現一個類型安全的接口IEquatable,所以Person類型的最終版本應該如下所示:
class Person : IEquatable<Person> { public string IDCode { get; PRivate set; } public Person(string idCode) { this.IDCode = idCode; } public override bool Equals(object obj) { return IDCode == (obj as Person).IDCode; } public override int GetHashCode() { return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode(); } public bool Equals(Person other) { return IDCode == other.IDCode; } }
轉自:《編寫高質量代碼改善C#程序的157個建議》陸敏技
新聞熱點
疑難解答