一、C# Dictionary的key可以是一個自定義的struct嗎?
在C#中,Dictionary<TKey, TValue>
的 TKey
可以是任何型別,包括自定義的 struct
(結構體)。但必須滿足:
- 不可變:作為鍵的
struct
一旦建立,欄位值不能被修改。字典的鍵需要保持不變,以便字典正確地定位和檢索值。 - 重寫
GetHashCode
方法:確保相同struct
的內容生成的雜湊碼保持一致。 - 重寫
Equals
方法:字典中查詢鍵時用於比較兩個struct
是否相等
using System;
using System.Collections.Generic;
public struct MyKey
{
public int Part1;
public int Part2;
public MyKey(int part1, int part2)
{
Part1 = part1;
Part2 = part2;
}
public override bool Equals(object obj)
{
if (obj is MyKey)
{
MyKey other = (MyKey)obj;
return Part1 == other.Part1 && Part2 == other.Part2;
}
return false;
}
public override int GetHashCode()
{
return Part1.GetHashCode() ^ Part2.GetHashCode();
}
}
二、實現IEquatable<T>介面保所有相等性測試返回一致的結果
實現IEquatable<T>
宣告瞭一個實現例項比較的函式bool IEquatable<MyClass>.Equals(MyClass other)
必須要被實現,具體有如下兩種實現方法:
-
顯式實現(Explicit Implementation):
顯式實現介面成員意味著這些成員只在介面內部可見,對於類的例項呼叫是不可見的。顯式實現的Equals
方法只能透過IEquatable<MyClass>
介面的引用來呼叫:public class MyClass : IEquatable<MyClass> { // 顯式實現介面的Equals方法 bool IEquatable<MyClass>.Equals(MyClass other) { // 實現比較邏輯 return this.Property == other.Property; } }
-
隱式實現(Implicit Implementation):
隱式實現的介面成員同時是類的公共成員。當類的成員與介面的成員具有相同的簽名時,這些成員可以隱式地實現介面。這種方式使得介面的成員可以直接透過類的例項呼叫。public class MyClass : IEquatable<MyClass> { // 隱式實現介面的Equals方法: public bool Equals(MyClass other) { // 實現比較邏輯 return this.Property == other.Property; } }
所謂的隱式實現指的就是當
bool IEquatable<MyClass>.Equals(MyClass other)
沒有被實現的時候會用public bool Equals(MyClass other)
來替代執行Equals相等操作。
實現IEquatable則語法上bool IEquatable .Equals(MyKey other)與public bool Equals(MyKey other)必須要至少實現一個
確保所有相等性測試返回一致的結果
實現IEquatable<T>
,還應重寫GetHashCode()
和Equals(Object)
的基類實現,使其行為與方法Equals(T)
的行為一致。 如果確實重寫Equals(Object)
,則類上的靜態方法Equals(System.Object, System.Object)
的呼叫中會呼叫重寫的實現。 此外,應過載op_Equality
和op_Inequality
運算子。 這可確保所有相等性測試返回一致的結果。
說白了就是實現IEquatable<T>
的時候把如下幾個函式都重寫一遍:
bool IEquatable<T>.Equals(T other){...}
public bool Equals(T other){...}
public override bool Equals(object obj){...}
public static bool operator !=(T left, T right){...}
public static bool operator ==(T left, T right){...}
public override int GetHashCode(){...}
其中bool IEquatable<T>.Equals(T other)
可有可無,因為不存在的情況下會自動被public bool Equals(T other){...}
替代
範例
using System;
namespace MyNamespace
{
public struct MyKey : IEquatable<MyKey>
{
public int Part1;
public int Part2;
public MyKey(int part1, int part2)
{
Part1 = part1;
Part2 = part2;
}
bool IEquatable<MyKey>.Equals(MyKey other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Part1 == this.Part1 && other.Part2 == this.Part2;
}
public bool Equals(MyKey other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Part1 == this.Part1 && other.Part2 == this.Part2;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return ((MyKey)this).Equals((MyKey)obj);
}
public static bool operator !=(MyKey left, MyKey right)
{
return !left.Equals(right);
}
public static bool operator ==(MyKey left, MyKey right)
{
return left.Equals(right);
}
public override int GetHashCode()
{
int hash = 17;
hash = hash * 23 + Part1;
hash = hash * 23 + Part2;
return hash;
}
}
class Program
{
static void Main(string[] args)
{
MyKey key1 = new MyKey(1, 2);
MyKey key2 = new MyKey(1, 2);
IEquatable<MyKey> other = new MyKey(1, 2);
key1.Equals(key2);
// 上面這一行會呼叫public bool Equals(MyKey other)
key1.Equals(other);
// 上面這一行會調public override bool Equals(object obj)
other.Equals(key1);
// 實現IEquatable<MyKey>則語法上bool IEquatable<MyKey>.Equals(MyKey other)與public bool Equals(MyKey other)必須要至少實現一個
// 如果bool IEquatable<MyKey>.Equals(MyKey other)被實現了上面這一行會調bool IEquatable<MyKey>.Equals(MyKey other)
// 如果bool IEquatable<MyKey>.Equals(MyKey other)沒有被實現上面這一行會調public bool Equals(MyKey other)
Console.ReadKey();
}
}
}
三、結構體GetHashCode函式實現方法
通常的做法是將這些數值成員的雜湊程式碼組合起來生成一個唯一的雜湊程式碼。這樣,即使結構體的成員型別不同,也可以生成一個相對一致的雜湊值:
using System;
public struct MyStruct
{
public int IntMember;
public double DoubleMember;
public long LongMember;
public override int GetHashCode()
{
unchecked // Overflow is fine, just wrap
{
int hash = 17;
// Suitable nullity checks etc, of course :)
hash = hash * 23 + IntMember.GetHashCode();
hash = hash * 23 + DoubleMember.GetHashCode();
hash = hash * 23 + LongMember.GetHashCode();
return hash;
}
}
}
這裡GetHashCode
方法首先初始化一個基本的雜湊值,然後透過將每個成員的雜湊值乘以一個質數(這裡使用23)並加上成員的雜湊值來組合雜湊值。使用unchecked
上下文允許整數溢位,這通常在雜湊程式碼計算中是可以接受的。
對於double
型別的成員,直接呼叫GetHashCode
可能會因為浮點數的精度問題導致不同的double
值產生相同的雜湊程式碼。如果需要,你可以先將double
值轉換為一種更穩定的表示形式,比如將其位模式轉換為long
型別,然後再計算雜湊值。
對於int
型別的成員,GetHashCode
其實就是返回其自身的值,我們也可以直接使用值進行計算
但是這裡就有個問題,一方面這樣實現方法過於複雜,另外一方面每個成員都呼叫一個自身的GetHashCode
也會導致效率很低
高效的方法是儘量透過直接計算成員的雜湊值,而不是使用GetHashCode
方法。這樣可以避免不必要的方法呼叫開銷。更好的處理方法是使用HashCode.Combine
public override int GetHashCode()
{
return HashCode.Combine(Member1, Member2, Member3, Member4);
}
HashCode.Combine
可以減少手動計算雜湊值的複雜性,並提高程式碼的可讀性。這種方法在處理多個成員時特別有用,因為它自動處理了雜湊值的組合,這種方法有幾個優點:
- 簡潔性:程式碼更加簡潔,易於閱讀和維護。
- 效率:
HashCode.Combine
內部實現了高效的雜湊碼組合演算法,減少了手動計算的需要。 - 可擴充套件性:可以很容易地新增或刪除要組合的成員,而不需要修改雜湊碼計算的邏輯。
然而,使用 HashCode.Combine
也有一些需要注意的地方:
- 型別轉換:如果組合的值中包含非
int
型別的值(如示例中的long
),則需要顯式轉換為int
。這可能會導致資料丟失,特別是當值超出int
的範圍時。在這種情況下,你可能需要考慮其他方法來處理值,以避免雜湊衝突。 - 引數順序:
HashCode.Combine
對引數的順序敏感。如果你更改了結構體成員的順序,你需要相應地更新GetHashCode
方法中的引數順序,以確保雜湊碼的一致性。