重寫Equals時也要重寫GetHashCode

建議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;  
    }  
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章