C# IEquatable<T>介面與可用與作Key的條件

蛮哥哥發表於2024-06-29

一、C# Dictionary的key可以是一個自定義的struct嗎?

在C#中,Dictionary<TKey, TValue>TKey 可以是任何型別,包括自定義的 struct(結構體)。但必須滿足:

  1. 不可變:作為鍵的 struct一旦建立,欄位值不能被修改。字典的鍵需要保持不變,以便字典正確地定位和檢索值。
  2. 重寫 GetHashCode 方法:確保相同 struct 的內容生成的雜湊碼保持一致。
  3. 重寫 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)必須要被實現,具體有如下兩種實現方法:

  1. 顯式實現(Explicit Implementation):
    顯式實現介面成員意味著這些成員只在介面內部可見,對於類的例項呼叫是不可見的。顯式實現的Equals方法只能透過IEquatable<MyClass>介面的引用來呼叫:

    public class MyClass : IEquatable<MyClass>
    {
        // 顯式實現介面的Equals方法
        bool IEquatable<MyClass>.Equals(MyClass other)
        {
            // 實現比較邏輯
            return this.Property == other.Property;
        }
    }
    
  2. 隱式實現(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_Equalityop_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可以減少手動計算雜湊值的複雜性,並提高程式碼的可讀性。這種方法在處理多個成員時特別有用,因為它自動處理了雜湊值的組合,這種方法有幾個優點:

  1. 簡潔性:程式碼更加簡潔,易於閱讀和維護。
  2. 效率HashCode.Combine 內部實現了高效的雜湊碼組合演算法,減少了手動計算的需要。
  3. 可擴充套件性:可以很容易地新增或刪除要組合的成員,而不需要修改雜湊碼計算的邏輯。

然而,使用 HashCode.Combine 也有一些需要注意的地方:

  • 型別轉換:如果組合的值中包含非 int 型別的值(如示例中的 long),則需要顯式轉換為 int。這可能會導致資料丟失,特別是當值超出 int 的範圍時。在這種情況下,你可能需要考慮其他方法來處理值,以避免雜湊衝突。
  • 引數順序HashCode.Combine 對引數的順序敏感。如果你更改了結構體成員的順序,你需要相應地更新 GetHashCode 方法中的引數順序,以確保雜湊碼的一致性。

相關文章