c# - 當Equals方法被覆蓋時為什麼重寫GetHashCode很重要?




override (8)

只需添加上面的答案:

如果您不覆蓋Equals,那麼默認行為是比較對象的引用。 這同樣適用於散列碼 - 默認實現通常基於引用的內存地址。 因為您確實覆蓋了Equals,所以它的正確行為是比較您在Equals上執行的操作,而不是引用,因此您應該對哈希碼執行相同的操作。

您的類的客戶端會希望哈希碼與equals方法具有相似的邏輯,例如,使用IEqualityComparer的linq方法首先比較哈希碼,並且只有在它們相等時,才會比較可能更昂貴的Equals()方法運行,如果我們沒有實現哈希碼,等於對象可能會有不同的哈希碼(因為它們有不同的內存地址),並且會被錯誤地判定為不相等(Equals()甚至不會被命中)。

另外,除瞭如果你在字典中使用它,你可能無法找到你的對象的問題(因為它是由一個散列碼插入的,當你查找它時,默認的散列碼可能會不同,並且Equals()甚至不會被稱為,就像Marc Gravell在他的回答中解釋的那樣,你還介紹了違反字典或哈希集的概念,它不應該允許使用相同的鍵 - 你已經聲明這些對象基本上是相同的,當你重寫Equals時,並不希望它們都是假設擁有唯一密鑰的數據結構上的不同密鑰,但由於它們具有不同的哈希碼,所以“相同”密鑰將被插入為不同的密鑰。

鑑於以下課程

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Which is preferred?

        return base.GetHashCode();

        //return this.FooId.GetHashCode();
    }
}

我已經重寫了Equals方法,因為Foo表示Foo表的一行。 哪個是重寫GetHashCode的首選方法?

為什麼重寫GetHashCode很重要?


實際上很難正確實現GetHashCode() ,因為除了Marc已經提到的規則外,哈希碼在對象的生命週期內不應該改變。 因此,用於計算哈希代碼的字段必須是不可變的。

當我使用NHibernate時,終於找到了解決這個問題的辦法。 我的方法是從對象的ID中計算哈希代碼。 ID只能通過構造函數設置,所以如果你想改變ID,這是不太可能的,你必須創建一個新的對象,它有一個新的ID,因此一個新的哈希碼。 這種方法最適合於GUID,因為您可以提供一個隨機生成ID的無參數構造函數。


我們有兩個問題需要解決。

  1. 如果可以更改對像中的任何字段,則無法提供明智的GetHashCode() 。 也永遠不會在依賴於GetHashCode()的集合中使用對象。 所以實現GetHashCode()的成本往往不值得,或者這是不可能的。

  2. 如果有人將你的對象放入一個調用GetHashCode()的集合中,並且你沒有使GetHashCode()以正確的方式運行,那麼你可能會花費數天的時間來追踪這個問題。

因此,我默認。

public class Foo
{
    public int FooId { get; set; }
    public string FooName { get; set; }

    public override bool Equals(object obj)
    {
        Foo fooItem = obj as Foo;

        return fooItem.FooId == this.FooId;
    }

    public override int GetHashCode()
    {
        // Some comment to explain if there is a real problem with providing GetHashCode() 
        // or if I just don't see a need for it for the given class
        throw new Exception("Sorry I don't know what GetHashCode should do for this class");
    }
}

我的理解是,原始的GetHashCode()返回對象的內存地址,所以如果你想比較兩個不同的對象,就必須重寫它。

編輯:這是不正確的,原始的GetHashCode()方法不能保證2值的相等。 儘管相同的對象返回相同的哈希碼。


是的,如果您的項目將用作字典中的鍵或HashSet<T>等 - 這一點非常重要,因為這用於(在沒有自定義IEqualityComparer<T> )將項目分組為桶。 如果兩個項目的哈希碼不匹配,它們可能永遠不會被視為相等( Equals將永遠不會被調用)。

GetHashCode()方法應該反映Equals邏輯; 規則是:

  • 如果兩個事物相等( Equals(...) == true ),那麼它們必須GetHashCode()返回相同的值
  • 如果GetHashCode()是相等的,則它們不必是相同的; 這是一次碰撞, Equals將被調用以查看它是否是真正的平等。

在這種情況下,它看起來像“ return FooId; ”是一個合適的GetHashCode()實現。 如果您正在測試多個屬性,通常使用下面的代碼將它們組合起來以減少對角線衝突(即,使new Foo(3,5)具有與new Foo(5,3)不同的散列碼):

int hash = 13;
hash = (hash * 7) + field1.GetHashCode();
hash = (hash * 7) + field2.GetHashCode();
...
return hash;

哦 - 為了方便起見,當重寫EqualsGetHashCode時,您可能還會考慮提供==!=運算符。

當你發現這個錯誤時會發生什麼的證明就here


考慮到公共財產,使用反射的下面似乎是一個更好的選擇,因為您不必擔心增加/移除屬性(儘管不是那麼常見的情況)。 這發現我的表現也更好。(比較使用Diagonistics秒錶的時間)。

    public int getHashCode()
    {
        PropertyInfo[] theProperties = this.GetType().GetProperties();
        int hash = 31;
        foreach (PropertyInfo info in theProperties)
        {
            if (info != null)
            {
                var value = info.GetValue(this,null);
                if(value != null)
                unchecked
                {
                    hash = 29 * hash ^ value.GetHashCode();
                }
            }
        }
        return hash;  
    }

這是因為框架要求兩個相同的對象必須具有相同的哈希碼。 如果您重寫equals方法來對兩個對象進行特殊比較,並且該方法將兩個對象視為相同,則兩個對象的哈希碼也必須相同。 (字典和哈希表依賴於這個原則)。


通過覆蓋Equals,你基本上聲明你是一個知道如何比較給定類型的兩個實例的人,所以你很可能是提供最佳散列碼的最佳人選。

這是ReSharper如何為你寫一個GetHashCode()函數的例子:

public override int GetHashCode()
{
    unchecked
    {
        var result = 0;
        result = (result * 397) ^ m_someVar1;
        result = (result * 397) ^ m_someVar2;
        result = (result * 397) ^ m_someVar3;
        result = (result * 397) ^ m_someVar4;
        return result;
    }
}

正如你所看到的,它只是試圖根據類中的所有字段猜測一個好的哈希碼,但由於你知道你的對象的域或值範圍,你仍然可以提供更好的哈希碼。





hashcode