工作中的 C# 實作:GetHashCode 的用處
前言
在 C# 中,GetHashCode()
是一個非常重要的方法,尤其是在處理資料集合時。理解它的作用、實作方式以及背後原理,能幫助你寫出更高效能、更穩健的程式碼。
1. 什麼是 Hash Code (雜湊值)?
想像一下,每個物件都有一個專屬的「身分證字號」,用來快速區分不同的物件,這個「身分證字號」就是 Hash Code。它是一個整數值 (int
),由 GetHashCode()
方法計算並回傳。
為什麼需要 Hash Code?
Hash Code 主要用在雜湊表 (hash table) 資料結構中,例如 Dictionary<TKey, TValue>
和 HashSet<T>
。
這些集合利用 Hash Code 來快速查找物件。當你把一個物件放進 Dictionary
時,它會先計算物件的 Hash Code,然後根據這個值把物件存放在一個特定的「桶子」(bucket) 裡。當要查找時,它會用同樣的方式計算 Hash Code,直接找到那個桶子,而不需要一個一個地比對所有物件,這大大提升了查找效率,讓操作接近於 O(1)
(常數時間)。
2. 為什麼需要覆寫 GetHashCode()
?
每個 C# 類別都繼承自 Object
類別,而 Object
類別本身就有一個預設的 GetHashCode()
方法。然而,這個預設實作通常是基於物件在記憶體中的位址,這會導致兩個內容完全相同的物件,因為它們的記憶體位址不同,而有不同的 Hash Code。
這在物件比較時會造成問題。根據微軟的規範,如果你覆寫了 Equals()
方法來定義兩個物件是否「相等」,就必須同時覆寫 GetHashCode()
方法。
而且,以下這個重要原則必須被遵守:
如果兩個物件經
Equals()
判斷為true
,那麼它們的GetHashCode()
回傳的值也必須相同。
3. 如何實作一個好的 GetHashCode()
?
一個好的 GetHashCode()
實作應該具備以下特性:
- 穩定性:只要用來計算 Hash Code 的屬性沒有改變,
GetHashCode()
的回傳值就應該永遠相同。 - 高效能:計算過程要快速,避免複雜的運算。
- 均勻分佈:產生的 Hash Code 應該盡量均勻分佈,減少雜湊碰撞 (Hash Collision) 的發生。
什麼是雜湊碰撞 (Hash Collision)?
雜湊碰撞是指兩個不同的物件,卻產生了相同的 Hash Code。這就像不同的人卻有相同的身分證字號一樣。
當雜湊碰撞發生時,Dictionary
必須把這兩個物件都放進同一個桶子裡。此時,當你要查找物件時,即使找到了對應的桶子,也必須在桶子內部一個一個地比較每一個物件,這會降低查找效率,失去了雜湊表的核心優勢。
4. 實作 GetHashCode()
的方法
4.1 現代推薦寫法:使用 HashCode.Combine()
從 .NET Core 5+ 開始,微軟提供了 HashCode.Combine()
這個靜態方法。這是目前最簡單、最安全、也最推薦的實作方式。它會自動處理多個欄位的 Hash Code 混合,並避免常見的錯誤 (例如 null
檢查)。
1 | public class Person |
4.2 傳統寫法:質數混合法
在 HashCode.Combine()
出現之前,一種廣泛使用的實作方式是透過質數來混合各個欄位的 Hash Code。這種方法的核心思想是,透過乘上一個質數來「混合」各個 Hash Code,以達到更好的分佈效果。
通常會選擇兩個質數:一個是基底 (例如 17
),一個是乘數 (例如 23
或 397
)。
1 | public override int GetHashCode() |
注意:
unchecked
關鍵字:用於避免因整數溢位而拋出例外。- 質數選擇:
17
和23
是經驗法則中表現不錯的組合。選擇質數的原因是它們可以讓 Hash Code 的分佈更均勻,減少碰撞。
4.3 另一種現代寫法:使用元組 (Tuple)
元組 (Tuple) 的 GetHashCode()
已經幫你處理好了,你可以直接利用這個特性來實作。
1 | public override int GetHashCode() |
5. 錯誤的實作方式
5.1 只使用一個屬性
如果你只用一個屬性來計算 Hash Code,會大幅提高碰撞的機率。
1 | // 不好的實作範例 |
5.2 回傳常數值
回傳常數值會導致所有物件的 Hash Code 都相同,這會讓雜湊表完全失去效能優勢,退化成一個鏈結串列 (Linked List),查找效率從 O(1)
變成 O(n)
。
1 | // 絕對不要這樣實作 |
總結
GetHashCode()
用於為物件產生一個整數值,以優化雜湊表的查找效能。- 如果你覆寫
Equals()
,就必須覆寫GetHashCode()
。 - 推薦使用
HashCode.Combine()
或元組來實作,這兩者都安全、簡潔且高效。傳統使用質數混合法。 - 避免只依賴單一屬性或回傳常數值,這會導致效能問題。