工作中的 C# 實作:GetHashCode 的用處

AdSense

前言

在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }

// 這裡實作 Equals 方法...

public override int GetHashCode()
{
// 推薦使用!只需將所有相關屬性傳入即可
return HashCode.Combine(FirstName, LastName, Age);
}
}

4.2 傳統寫法:質數混合法

HashCode.Combine() 出現之前,一種廣泛使用的實作方式是透過質數來混合各個欄位的 Hash Code。這種方法的核心思想是,透過乘上一個質數來「混合」各個 Hash Code,以達到更好的分佈效果。

通常會選擇兩個質數:一個是基底 (例如 17),一個是乘數 (例如 23397)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public override int GetHashCode()
{
// 確保不會拋出溢位例外
unchecked
{
// 初始化基底
int hash = 17;

// 透過乘上質數來混合每個欄位的 Hash Code
hash = hash * 23 + (FirstName != null ? FirstName.GetHashCode() : 0);
hash = hash * 23 + (LastName != null ? LastName.GetHashCode() : 0);
hash = hash * 23 + Age.GetHashCode();

return hash;
}
}

注意:

  • unchecked 關鍵字:用於避免因整數溢位而拋出例外。
  • 質數選擇: 1723 是經驗法則中表現不錯的組合。選擇質數的原因是它們可以讓 Hash Code 的分佈更均勻,減少碰撞。

4.3 另一種現代寫法:使用元組 (Tuple)

元組 (Tuple) 的 GetHashCode() 已經幫你處理好了,你可以直接利用這個特性來實作。

1
2
3
4
5
public override int GetHashCode()
{
// 利用元組來自動處理 Hash Code 的組合
return (FirstName, LastName, Age).GetHashCode();
}

5. 錯誤的實作方式

5.1 只使用一個屬性

如果你只用一個屬性來計算 Hash Code,會大幅提高碰撞的機率。

1
2
3
4
5
// 不好的實作範例
public override int GetHashCode()
{
return FirstName.GetHashCode();
}

5.2 回傳常數值

回傳常數值會導致所有物件的 Hash Code 都相同,這會讓雜湊表完全失去效能優勢,退化成一個鏈結串列 (Linked List),查找效率從 O(1) 變成 O(n)

1
2
3
4
5
// 絕對不要這樣實作
public override int GetHashCode()
{
return 0;
}

總結

  • GetHashCode() 用於為物件產生一個整數值,以優化雜湊表的查找效能。
  • 如果你覆寫 Equals()就必須覆寫 GetHashCode()
  • 推薦使用 HashCode.Combine() 或元組來實作,這兩者都安全、簡潔且高效。傳統使用質數混合法。
  • 避免只依賴單一屬性或回傳常數值,這會導致效能問題。