一文詳解.NET中GetHashCode方法的正確使用方式
一、一段“詭異”的去重代碼
先看下面這段代碼,它嘗試對(duì) hr_data_approve 對(duì)象集合按 userid、begintime、endtime 三個(gè)字段的組合進(jìn)行去重:
public class ApproveDistinctCompare : IEqualityComparer<hr_data_approve>
{
public bool Equals(hr_data_approve x, hr_data_approve y)
{
return x.begintime == y.begintime && x.endtime == y.endtime;
}
public int GetHashCode([DisallowNull] hr_data_approve obj)
{
return obj.userid.GetHashCode();
}
}
leaveList = leaveList.Distinct(new ApproveDistinctCompare()).ToList();乍一看似乎沒什么問題,但實(shí)際運(yùn)行后去重結(jié)果完全不符合預(yù)期:有時(shí)本該去重的記錄留了下來,有時(shí)不該去重的卻被刪掉了。原因就藏在 GetHashCode 和 Equals 的不一致上。
二、HashCode是什么?為何如此重要?
2.1 哈希碼的定義
GetHashCode 返回一個(gè) int 類型的數(shù)值,可以理解為對(duì)象的“短指紋”。它的核心作用是為基于哈希表的集合(如 HashSet<T>、Dictionary<TKey,TValue>、LINQ 的 Distinct() 等)提供快速定位能力。
哈希表的工作原理大致是:
- 插入元素時(shí),先調(diào)用
GetHashCode得到哈希碼,通過hashCode % bucketCount決定將元素放入哪個(gè)“桶”。 - 查找時(shí),同樣計(jì)算哈希碼,直接定位到對(duì)應(yīng)的桶,然后在桶內(nèi)使用
Equals逐個(gè)比較。
2.2 哈希碼的黃金法則
如果 Equals(a, b) 返回 true,那么 a.GetHashCode() 必須等于 b.GetHashCode()。
反之不要求(不同對(duì)象可以哈希碼相同,這叫“碰撞”)。
違反這一法則,哈希表的行為會(huì)變得完全不可預(yù)測(cè)——因?yàn)閮蓚€(gè)“相等”的對(duì)象可能被分配到不同的桶,導(dǎo)致 Equals 永遠(yuǎn)不會(huì)被調(diào)用,從而誤判為不相等。
三、原代碼錯(cuò)在哪?
| 方法 | 期望(場(chǎng)景二) | 原代碼實(shí)現(xiàn) | 后果 |
|---|---|---|---|
Equals | 比較 userid、begintime、endtime | 只比較 begintime 和 endtime | 不同用戶只要時(shí)間相同就被誤判為相等 |
GetHashCode | 基于三個(gè)字段計(jì)算 | 只基于 userid 計(jì)算 | 相同用戶不同時(shí)間的對(duì)象哈希碼相同,導(dǎo)致大量碰撞,性能下降,且與 Equals 邏輯割裂 |
更嚴(yán)重的是,由于 Equals 和 GetHashCode 用的字段完全不同,違反了哈希碼黃金法則:兩個(gè)具有相同 begintime/endtime 但不同 userid 的對(duì)象,Equals 返回 true,而 GetHashCode 因?yàn)?userid 不同而返回不同值。這會(huì)讓 Distinct 內(nèi)部判斷邏輯混亂,去重結(jié)果隨機(jī)錯(cuò)誤。
四、正確的GetHashCode應(yīng)該怎么寫?
針對(duì)場(chǎng)景二(基于 userid、begintime、endtime 三者組合去重),正確的實(shí)現(xiàn)如下:
public class ApproveDistinctCompare : IEqualityComparer<hr_data_approve>
{
public bool Equals(hr_data_approve x, hr_data_approve y)
{
if (ReferenceEquals(x, y)) return true;
if (x is null || y is null) return false;
return x.userid == y.userid
&& x.begintime == y.begintime
&& x.endtime == y.endtime;
}
public int GetHashCode([DisallowNull] hr_data_approve obj)
{
if (obj is null) throw new ArgumentNullException(nameof(obj));
unchecked
{
int hash = 17;
hash = hash * 31 + (obj.userid?.GetHashCode() ?? 0);
hash = hash * 31 + (obj.begintime?.GetHashCode() ?? 0);
hash = hash * 31 + (obj.endtime?.GetHashCode() ?? 0);
return hash;
}
}
}4.1 代碼逐行解讀
unchecked關(guān)鍵字
- 哈希計(jì)算涉及乘法,
int很容易溢出。unchecked允許溢出時(shí)自動(dòng)回繞(wrap around),這是哈希算法的正常現(xiàn)象,無需拋出異常。
為什么選17和31?
- 這兩個(gè)數(shù)是素?cái)?shù),使用素?cái)?shù)可以降低哈希碰撞的概率。
31是經(jīng)典的乘數(shù)(Java 的Objects.hash()也用 31),因?yàn)?31 * i可被 JIT 優(yōu)化為(i << 5) - i,位運(yùn)算效率高。
為什么每次都乘以31?
- 避免不同字段順序產(chǎn)生相同的哈希值。比如
("A","B")和("B","A")如果僅累加,會(huì)得到相同結(jié)果;而乘以質(zhì)數(shù)再加新字段,可以讓順序影響最終哈希值。
obj.userid?.GetHashCode() ?? 0
- 處理字段可能為
null的情況:當(dāng)userid為null時(shí),?.阻止調(diào)用GetHashCode,表達(dá)式返回null,?? 0將其替換為0。這樣既安全又不會(huì)拋空引用異常。
為什么要用三個(gè)字段?
- 必須與
Equals基于完全相同的字段組合。因?yàn)?Equals判斷三個(gè)字段全部相等才返回true,所以只有當(dāng)三個(gè)字段都相同時(shí),哈希碼也必須相同。這是契約的要求。
4.2 簡(jiǎn)化寫法(.NET Core 2.1+)
如果你使用的框架版本支持 System.HashCode,可以大幅簡(jiǎn)化:
public override int GetHashCode() => HashCode.Combine(userid, begintime, endtime);
VSCode / Visual Studio 還提供了自動(dòng)生成 Equals 和 GetHashCode 的功能(右鍵 → 快速操作 → 生成 Equals/GetHashCode),非常推薦使用。
五、常見誤區(qū)與最佳實(shí)踐
誤區(qū)1:只在Equals里寫邏輯,GetHashCode隨便返回一個(gè)常數(shù)
- 后果:所有對(duì)象哈希碼相同,全部落入同一個(gè)桶,哈希表退化成鏈表,性能從 O(1) 變成 O(n)。
誤區(qū)2:用可變字段參與哈希碼計(jì)算
- 后果:對(duì)象加入
HashSet后,如果參與哈希碼的字段被修改,該對(duì)象在哈希表內(nèi)的位置就會(huì)“丟失”,再也無法被查找或刪除。推薦僅用不可變字段(如 Id、創(chuàng)建時(shí)間)計(jì)算哈希碼。
誤區(qū)3:不處理null值
- 后果:當(dāng)字段為
null時(shí)調(diào)用其GetHashCode會(huì)拋出NullReferenceException。始終使用?.GetHashCode() ?? 0或顯式判斷。
最佳實(shí)踐總結(jié)
Equals和GetHashCode必須基于完全相同的字段組合。- 使用質(zhì)數(shù)(如 17、31)作為初始值和乘數(shù),降低碰撞率。
- 用
unchecked處理溢出。 - 處理可能為
null的字段。 - 優(yōu)先使用
HashCode.Combine或 IDE 生成工具。 - 哈希碼計(jì)算中使用的字段應(yīng)為只讀(或至少不應(yīng)在對(duì)象作為哈希表鍵時(shí)被修改)。
六、結(jié)語
GetHashCode 看起來只是簡(jiǎn)單的整數(shù)計(jì)算,但它與 Equals 共同構(gòu)成了 .NET 中所有哈希集合的基石。一個(gè)小小的不一致,就可能讓 Distinct、HashSet、Dictionary 出現(xiàn)匪夷所思的錯(cuò)誤。下次再遇到“明明數(shù)據(jù)一樣,為什么去重?zé)o效”的問題,請(qǐng)第一時(shí)間檢查 GetHashCode 和 Equals 是否“言行一致”。
記住黃金法則:
// 如果以下代碼輸出 true,那么 hash1 必須等于 hash2 bool equal = comparer.Equals(a, b); int hash1 = comparer.GetHashCode(a); int hash2 = comparer.GetHashCode(b);
希望這篇文章能幫你徹底理解哈希碼,寫出健壯、高效的自定義比較器。
以上就是一文詳解.NET中GetHashCode方法的正確使用方式的詳細(xì)內(nèi)容,更多關(guān)于.NET GetHashCode正確使用方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
.NET事件監(jiān)聽機(jī)制的局限與擴(kuò)展分析
這篇文章主要介紹了.NET事件監(jiān)聽機(jī)制的局限與擴(kuò)展,詳細(xì)分析了.NET事件監(jiān)聽機(jī)制的機(jī)制與優(yōu)劣,有助于更好的理解.NET的運(yùn)行原理,需要的朋友可以參考下2014-11-11
asp.net UrlReWriter使用經(jīng)驗(yàn)小結(jié)
UrlRewriter 是微軟封裝好了的一個(gè)URL重寫組件。使用它可以讓我節(jié)約很多自已開發(fā)的時(shí)間。 好了,開始講述我的應(yīng)用經(jīng)驗(yàn),這只是很菜鳥的經(jīng)驗(yàn),高手就不用看了。2008-11-11
ASP.NET?Core?MVC緩存Tag?Helpers到內(nèi)存
這篇文章介紹了ASP.NET?Core?MVC緩存Tag?Helpers到內(nèi)存的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-02-02
.Net使用SuperSocket框架實(shí)現(xiàn)WebSocket后端
這篇文章介紹了.Net使用SuperSocket框架實(shí)現(xiàn)WebSocket后端,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-01-01
ASP.NET Core中ResourceFilter過濾器的實(shí)現(xiàn)
ASP.NET Core 中的?ResourceFilter?是一種非常有用的過濾器類型,允許開發(fā)人員在請(qǐng)求到達(dá)控制器操作方法之前或響應(yīng)返回之前執(zhí)行一些操作,下面就來介紹一下,感興趣的可以了解一下2025-05-05
MVC4制作網(wǎng)站教程第三章 瀏覽用戶組操作3.1
這篇文章主要為大家詳細(xì)介紹了MVC4制作網(wǎng)站教程,瀏覽用戶組功能的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
解決.net項(xiàng)目中上傳的圖片或者文件太大無法上傳問題
本文主要介紹了解決.net項(xiàng)目中上傳的圖片或者文件太大無法上傳問題的具有方法,具有很好的參考價(jià)值,有需要的朋友可以看下2016-12-12
asp.net 網(wǎng)絡(luò)硬盤實(shí)現(xiàn)分析
隨著網(wǎng)絡(luò)技術(shù)的日益普及和信息化建設(shè)的重視,網(wǎng)絡(luò)硬盤作為一種新型安全的網(wǎng)絡(luò)存儲(chǔ)系統(tǒng),已越來越受到人們的重視和喜歡。2011-02-02

