問題背景#
首先提一下之前專案開發時遇到的一個將自定義類型作為 Dictionary 鍵的坑。
專案中,我們有兩個業務類 BusinessA 和 BusinessB,因為某個需求,我們需要將這兩個類建立一個映射關係,故引入字典 Dictionary 數據結構將它們關聯起來:
private Dictionary<BusinessA, BusinessB> businessDic = new Dictionary<BusinessA, BusinessB>();
//對外提供查找方法,通過A從字典中找到B
public BusinessB FindB(BusinessA a)
{
if (businessDic.ContainsKey(a))
{
return businessDic[a];
}
return null;
}
因為專案是協同開發的,隨著業務邏輯不斷增加,在某些其他業務邏輯中對字典中的某些 Key 對象做了修改(BusinessA 對象的數據不變),之後當我們再次想要通過該對象查找對應的 BusniessB 對象時,卻發現返回 null。當時我們整個開發組排查了好久才發現這個 bug 的原因。
問題分析#
這個 bug 的根本原因在於沒有深入的理解 C# 中的 Dictionary 的底層存儲原理:
當你將一個鍵添加到字典中,Dictionary 會使用哈希函數計算該鍵的哈希碼,這是一個整數值,用於確定鍵在內部數組中的位置。
我們一開始以為如果將一個引用類型的對象作為 Dictionary 的 key,只要 key 對象的數據沒有變,那麼就可以通過 key 獲取對應的 value。
使用 Dictionary 時,Key 的必要條件#
在 C# 中,字典 (Dcitionary<Tkey,TValue>) 使用將 Key 來快速查找值 Value 時,字典的鍵必須滿足兩個重要的條件:
- 可比較性
鍵必須可以被正確地比較以確定其唯一性。字典內部使用 Equals 方法來比較鍵,確保相同的鍵映射到相同的值
- 不可變性
一旦鍵被添加到字典中,== 其值不能被改變 ==。如果鍵的狀態在被添加到字典後發生變化,字典的查找機制可能會失效,從而引發 bug.
使用引用類型作為 Key 的注意事項#
使用不穩定的鍵#
為了確保字典的正確性,鍵應該在字典操作期間保持不變。一般來說,應該避免在字典中修改鍵對象的狀態。推薦的做法是使用不可變對象作為鍵。你可以使用 readonly
字段或只讀屬性來保證鍵對象的不可變性
鍵的相等性比較#
確保同時並正確的實現你的 Equals
和 GetHashCode
方法。相等性比較應該考慮所有影響對象身份的字段,並且 GetHashCode
方法應該始終返回相同的值(如果對象的狀態沒有改變)。
可以看以下示例代碼的輸出:
public class BusinessA
{
public int id;
public string name;
public BusinessA(int _id,string _name)
{
id = _id;
name = _name;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
BusinessA another = obj as BusinessA;
if (another != null)
{
return id == another.id && name == another.name;
}
return false;
}
}
public class BusinessB
{
private string describe;
private string score;
public BusinessB(string _des,string _score)
{
describe = _des;
score = _score;
}
}
public class DictionaryDemo : MonoBehaviour
{
private Dictionary<BusinessA, BusinessB> businessDic = new Dictionary<BusinessA, BusinessB>();
private BusinessA bA;
private BusinessB bB;
private BusinessA bAA;
void Start()
{
int id = 100;
string name = "zzz";
//聲明兩個內部數據一樣的對象bA和bAA分別作為存儲的key,和查找的key
bA = new BusinessA(id, name);
bAA = new BusinessA(id, name);
//聲明value
bB = new BusinessB("yyy", "100");
//bA和bB將添加到字典中
businessDic.Add(bA, bB);
//使用bAA在字典中查找bB
BusinessB findB = FindB(bAA);
BusinessB findB1 = FindB(bA);
//比較findB 是否等於bB
Debug.Log(bB == findB);
}
public BusinessB FindB(BusinessA a)
{
if (businessDic.ContainsKey(a))
{
return businessDic[a];
}
return null;
}
}
控制台輸出:
由此可以,雖然兩個對象 bA 和 bAA 的內部成員數據相同,但是並沒有重寫 GetHashCode 方法,所以這相當於是兩個不同哈希值的對象,使用 bAA 無法找到 Value 返回 null,所以 bB 不等於 findB。
解決方案#
由此得出解決方案:在使用字典存儲鍵值對時,如果需要將自定義類型作為字典的鍵,那麼該自定義類型應該重寫並正確實現 GetHashCode 方法。
驗證:
我們在 BusinessA 中重寫 GetHashCode 方法:
public override int GetHashCode()
{
return string.Format("{0}-{1}", id, name).GetHashCode();
}
控制台輸出:
由此可知,雖然 bA 和 bAA 是不同的對象引用,但是重寫了 GetHashCode 方法之後,在字典查找時,就能正確匹配對應的 key,所以能找出 Value,findB 和 bB 相等。
==Equals 和 GetHashCode 方法缺一不可。==
總結#
代碼層面出現 bug 的時候,很多時候還是一些底層的邏輯沒有搞懂,平時還是要多測試多驗證多了解原理。所以說在使用字典作為存儲查詢數據結構時還是建議使用不可變類型作為鍵,如何值類型(int、float 等)或者常用的引用類型 string。如果一定要使用自定義類型作為字典的鍵,那麼應該注意兩點:1. 避免在字典操作期間修改鍵;2. 作為鍵 Key 的對象需要重寫並正確實現 Equals 和 GetHashCode 方法。通過,遵循這些原則,可以避免由於自定義類型對象作為字典鍵而引起的 bug.