簡體   English   中英

在沒有不可變字段的類中重寫Object.GetHashCode()時要返回什么?

[英]What to return when overriding Object.GetHashCode() in classes with no immutable fields?

好吧,在你因為互聯網上發布了數百個類似的聲音問題而瘋狂之前,我可以向你保證,我剛剛花了幾個小時閱讀所有這些問題並且沒有找到我的問題的答案。

背景:

基本上,我的一個大型應用程序遇到了ListBox.SelectedItem屬性上的某些Binding將停止工作或者在對當前所選項目進行編輯后程序崩潰的情況。 我最初問過'已經添加了相同密鑰的項目'從代碼問題中選擇ListBoxItem的例外情況 ,但沒有得到答案。

直到本周,我才有時間解決這個問題。 現在簡而言之,我找出了問題的原因。 這是因為我的數據類型類已經覆蓋了Equals方法,因此也覆蓋了GetHashCode方法。

現在對於那些不知道這個問題的人,我發現你只能使用不可變字段/屬性來實現GetHashCode方法。 使用Harvey Kwok對Overriding GetHashCode()帖子的回答摘錄來解釋這個:

問題是Dictionary和HashSet集合正在使用GetHashCode將每個項目放在存儲桶中。 如果基於某些可變字段計算哈希碼,並且在將對象放入HashSet或Dictionary后實際更改了字段,則無法再從HashSet或Dictionary中找到該對象。

所以實際問題是因為我在GetHashCode方法中使用了可變屬性。 當用戶在UI中更改這些屬性值時,對象的關聯哈希碼值會發生更改,然后在其集合中無法再找到項目。

題:

所以,我的問題是什么是處理我需要在沒有不可變字段的類中實現GetHashCode方法的情況的最佳方法? 對不起,讓我更加具體,因為這個問題已經被問過。

Overriding GetHashCode()帖子中的答案表明,在這些情況下,最好只返回一個常量值...一些建議返回值1 ,而其他建議返回一個素數。 就個人而言,我看不出這些建議之間有任何區別,因為我認為只有一個桶用於其中任何一個。

此外,Eric Lippert博客中關於GetHashCode指南和規則有一個標題為指南的部分:哈希碼的分布必須是“隨機的” ,這突出了使用導致使用不足的桶的算法的缺陷。 他警告說,算法會減少使用的桶數,並在桶變得非常大時導致性能問題 當然,返回常數屬於這一類。

我想到了為我的所有數據類型類(僅在C#中,而不是數據庫中)添加一個額外的Guid字段,專門用於GetHashCode方法。 所以我想在這個長篇介紹的最​​后,我的實際問題是哪個實現更好? 總結一下:

摘要:

在沒有不可變字段的類中重寫Object.GetHashCode()時,最好從GetHashCode方法返回一個常量,還是為每個類創建一個額外的readonly字段,僅用於GetHashCode方法? 如果我應該添加一個新字段,它應該是什么類型,我不應該將它包含在Equals方法中?

雖然我很高興收到任何人的答案,但我真的希望得到高級開發人員的答案,他們對這個主題有充分的了解。

回到基礎。 你看了我的文章; 再讀一遍。 與您的情況相關的兩個鐵定規則是:

  • 如果x等於y,那么x的哈希碼必須等於y的哈希碼。 等價地:如果x的哈希碼不等於y的哈希碼,那么x和y必須是不相等的。
  • 當x在哈希表中時,x的哈希碼必須保持穩定。

這些是正確性的要求。 如果你不能保證這兩件簡單的事情,那么你的程序將是不正確的。

你提出兩個解決方案。

您的第一個解決方案是始終返回常量。 這符合兩個規則的要求,但您將在哈希表中簡化為線性搜索。 你也可以使用一個列表。

您建議的另一個解決方案是以某種方式為每個對象生成哈希碼並將其存儲在對象中。 如果相等的項具有相同的哈希碼,則這是完全合法的。 如果您這樣做,那么您受到限制,如果哈希碼不同,則x等於y 必須為false。 這似乎使價值平等基本上不可能。 因為如果你想要引用相等性,你不會首先重寫Equals,這似乎是一個非常糟糕的主意,但是如果equals是一致的,它是合法的

我提出了第三種解決方案,即:永遠不要將對象放在哈希表中,因為哈希表首先是錯誤的數據結構。 哈希表的要點是快速回答問題“這組不可變值中的給定值是什么?” 並且您沒有一組不可變值 ,因此請勿使用哈希表。 使用正確的工具完成工作。 使用列表,並忍受線性搜索的痛苦。

第四個解決方案是:對用於相等的可變字段進行哈希處理,在每次變異之前從所有哈希表中刪除該對象,然后將其放回原處。 這符合兩個要求:哈希代碼同意相等,哈希表中的對象哈希是穩定的,並且您仍然可以快速查找。

我要么創建一個額外的readonly字段,要么拋出NotSupportedException 在我看來,另一種選擇毫無意義。 讓我們看看為什么。

不同(固定)哈希碼

提供不同的哈希碼很容易,例如:

class Sample
{
    private static int counter;
    private readonly int hashCode;

    public Sample() { this.hashCode = counter++; }

    public override int GetHashCode()
    {
        return this.hashCode;
    }

    public override bool Equals(object other)
    {
        return object.ReferenceEquals(this, other);
    }
}

從技術上講,你必須注意創造太多物體並在這里溢出counter ,但實際上我認為這對任何人來說都不會成為問題。

這種方法的問題是實例永遠不會比較平等。 但是,如果您只想將Sample實例用作其他類型的集合的索引,那就完全沒問題了。

常量哈希碼

如果存在不同實例應該比較的任何情況,那么乍看之下除了返回常量之外別無選擇。 但那會讓你離開?

在容器內定位實例將始終退化為等效的線性搜索。 因此,實際上通過返回常量,您允許用戶為您的類創建一個鍵控容器,但該容器將展示LinkedList<T>的性能特征。 對於熟悉你班級的人來說,這可能是顯而易見的,但我個人認為這是讓人們在腳下射擊。 如果您事先知道Dictionary不會像預期的那樣表現,那么為什么讓用戶創建一個呢? 在我看來,最好拋出NotSupportedException

但扔是你不能做的!

有些人會不同意上述情況,當這些人比自己聰明時,人們應該注意。 首先, 此代碼分析警告表明GetHashCode不應該拋出。 這是值得考慮的事情,但我們不要教條。 有時你必須打破規則是有原因的。

然而,這還不是全部。 在他關於這個主題的博客文章中 ,Eric Lippert說如果你從GetHashCode內部拋出那么

由於性能原因,您的對象不能成為許多內部使用哈希表的LINQ到對象查詢的結果。

失去LINQ當然是一個無賴,但幸運的是,這條路並沒有結束。 使用散列表的許多(所有?)LINQ方法都有重載,它們接受在散列時使用的IEqualityComparer<T> 所以你實際上可以使用LINQ,但它會不那么方便。

最后,您必須自己權衡選項。 我的觀點是,最好使用白名單策略(在需要時提供IEqualityComparer<T> ),只要它在技術上可行,因為這會使代碼變得明確:如果有人試圖天真地使用該類,他們會得到一個有用的異常告訴他們發生了什么,並且在使用它的任何地方都可以看到相等比較器,使得類的非凡行為立即變得清晰。

我想要覆蓋Equals ,但是對象沒有合理的不可變“關鍵字”(無論出於何種原因,使整個對象不可變是沒有意義的),在我看來,只有一個“正確”的選擇:

  • 實現GetHashCode以散列與Equals使用的相同字段。 (這可能是所有領域。)
  • 記錄在字典中不得更改這些字段的文檔。
  • 相信用戶要么不將這些對象放在詞典中,要么遵守第二條規則。

(返回一個常量值會影響字典性能。拋出異常會導致太多有用的情況,其中對象被緩存但未被修改GetHashCode任何其他實現都是錯誤的。)

無論如何,這會讓用戶陷入麻煩,這可能是他們的錯。 (具體來說:使用不應該使用的字典,或者在上下文中使用模型類型,它們應該使用使用引用相等性的視圖模型類型。)

或許我不應該首先壓倒Equals

如果類真的不包含可以計算哈希值的常量,那么我會使用比GUID更簡單的東西。 只需使用在類中(或在包裝類中)保留的隨機數。

一種簡單的方法是將hashCode存儲在私有成員中,並在第一次使用時生成它。 如果您的實體不經常更改,並且您不會使用兩個不同的Equal(您的Equals方法返回true)的對象作為字典中的鍵,那么這應該沒問題:

private int? _hashCode;

public override int GetHashCode() {
   if (!_hashCode.HasValue)
      _hashCode = Property1.GetHashCode() ^ Property2.GetHashCode() etc... based on whatever you use in your equals method
   return _hashCode.Value;
}

但是,如果您有對象a和對象b,其中a.Equals(b)== true,並且您使用a作為鍵(詞典[a] = value)在詞典中存儲條目。
如果a沒有改變,那么dictionary [b]將返回值,但是,如果在將條目存儲在字典中之后更改a,則字典[b]很可能會失敗。 唯一的解決方法是在任何鍵更改時重新發送字典。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM