如何定義類別或結構的實值相等 (C# 程式設計指南)

記錄會自動實作實值相等。 如果您的類型建立資料模型,而且應該實作實值相等,則請考慮定義 record,而非 class

當您定義類別或結構時,需判斷是否有必要為類型建立實值相等 (或等價) 的自訂定義。 通常,如果您預期將該類型的物件新增至集合,或物件的主要目的是要儲存一組欄位或屬性,則會實作實值相等。 您可以根據對該類型中所有欄位和屬性的比較來定義實值相等,也可以根據子集來進行定義。

不論是哪一種情況,以及在類別和結構中,您的實作都應該遵循五個等價保證 (針對下列規則,假設 xyz 不是 Null):

  1. 自反屬性:x.Equals(x) 傳回 true

  2. 對稱屬性:x.Equals(y) 傳回與 y.Equals(x) 相同的值。

  3. 可轉移的屬性:如果 (x.Equals(y) && y.Equals(z)) 傳回 true,則 x.Equals(z) 會傳回 true

  4. 只要 x 和 y 所參考的物件沒有經過修改,後續叫用 x.Equals(y) 就會傳回相同的值。

  5. 任何非 Null 值不等於 Null。 不過,x 為 Null 時,x.Equals(y) 會擲回例外狀況。 根據 Equals 的引數,這會中斷規則 1 或 2。

任何您已定義的結構,皆有繼承自 Object.Equals(Object) 方法的 System.ValueType 覆寫的預設實作實值相等。 此實作使用反映來檢查類型中的所有欄位和屬性。 雖然此實作會產生正確的結果,但相較於您針對該類型特別撰寫的自訂實作卻慢得多。

對類別和結構而言,實值相等的實作細節並不同。 不過,類別和結構都需要相同的基本步驟來實作相等:

  1. 覆寫虛擬Object.Equals(Object) 方法。 在大部分情況下,實作 bool Equals( object obj ) 應該只會呼叫特定類型的 Equals 方法,這是 System.IEquatable<T> 介面的實作。 (請參閱步驟 2)。

  2. 透過提供類型專屬的 Equals 方法實作 System.IEquatable<T> 介面。 實際的等價比較是在這裡執行。 例如,您可能決定只比較類型中的一個或兩個欄位,以定義相等。 不會從 Equals 擲回例外狀況。 針對繼承相關類別:

    • 此方法只會檢查在類別中宣告的欄位。 它應該呼叫 base.Equals 以檢查基底類別中的欄位 (如果類型直接繼承自 Object,則請不要呼叫 base.Equals,因為 Object.Equals(Object)Object 實作會執行參考相等檢查。)

    • 只有在所比較變數的執行階段類型相同時,才應該將兩個變數視為相等。 此外,如果變數的執行階段和編譯時間類型不同,則請確定使用執行階段類型 Equals 方法的 IEquatable 實作。 確定執行階段類型一律正確比較的一個策略,就是只在 sealed 類別中實作 IEquatable。 如需詳細資訊,請參閱本文稍後的類別範例

  3. 選用但為建議動作︰多載 ==!= 運算子。

  4. 覆寫 Object.GetHashCode,以便有實值相等的兩個物件產生相同的雜湊碼。

  5. 選用︰若要支援「大於」或「小於」的定義,請為類型實作 IComparable<T> 介面,並同時多載 <=>= 運算子。

注意

您可以使用記錄來取得實值相等語意,而不需要任何不必要的重複使用程式碼。

類別範例

下列範例示範如何在類別 (參考型別) 中實作實值相等。

namespace ValueEqualityClass;

class TwoDPoint : IEquatable<TwoDPoint>
{
    public int X { get; private set; }
    public int Y { get; private set; }

    public TwoDPoint(int x, int y)
    {
        if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.X = x;
        this.Y = y;
    }

    public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);

    public bool Equals(TwoDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // If run-time types are not exactly the same, return false.
        if (this.GetType() != p.GetType())
        {
            return false;
        }

        // Return true if the fields match.
        // Note that the base class is not invoked because it is
        // System.Object, which defines Equals as reference equality.
        return (X == p.X) && (Y == p.Y);
    }

    public override int GetHashCode() => (X, Y).GetHashCode();

    public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}

// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
    public int Z { get; private set; }

    public ThreeDPoint(int x, int y, int z)
        : base(x, y)
    {
        if ((z < 1) || (z > 2000))
        {
            throw new ArgumentException("Point must be in range 1 - 2000");
        }
        this.Z = z;
    }

    public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);

    public bool Equals(ThreeDPoint p)
    {
        if (p is null)
        {
            return false;
        }

        // Optimization for a common success case.
        if (Object.ReferenceEquals(this, p))
        {
            return true;
        }

        // Check properties that this class declares.
        if (Z == p.Z)
        {
            // Let base class check its own fields
            // and do the run-time type comparison.
            return base.Equals((TwoDPoint)p);
        }
        else
        {
            return false;
        }
    }

    public override int GetHashCode() => (X, Y, Z).GetHashCode();

    public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
    {
        if (lhs is null)
        {
            if (rhs is null)
            {
                // null == null = true.
                return true;
            }

            // Only the left side is null.
            return false;
        }
        // Equals handles the case of null on right side.
        return lhs.Equals(rhs);
    }

    public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}

class Program
{
    static void Main(string[] args)
    {
        ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
        ThreeDPoint pointC = null;
        int i = 5;

        Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
        Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
        Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
        Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));

        TwoDPoint pointD = null;
        TwoDPoint pointE = null;

        Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);

        pointE = new TwoDPoint(3, 4);
        Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
        Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
        Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);

        System.Collections.ArrayList list = new System.Collections.ArrayList();
        list.Add(new ThreeDPoint(3, 4, 5));
        Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

/* Output:
    pointA.Equals(pointB) = True
    pointA == pointB = True
    null comparison = False
    Compare to some other type = False
    Two null TwoDPoints are equal: True
    (pointE == pointA) = False
    (pointA == pointE) = False
    (pointA != pointE) = True
    pointE.Equals(list[0]): False
*/

在類別 (參考型別) 上,Object.Equals(Object) 方法的預設實作都是執行參考相等比較,而不是實值相等檢查。 當實作器覆寫虛擬方法時,其目的是為了提供實值相等語意。

==!= 運算子可以和類別搭配使用,即使類別未多載這些運算子也一樣。 不過,預設行為是執行參考相等檢查。 在類別中,如果您多載 Equals 方法,您應該多載 ==!= 運算子,但這並非必要。

重要

上述範例程式碼可能不會以您預期的方式來處理每個繼承情節。 請考慮下列程式碼:

TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True

此程式碼報告除了 z 值中有所差異之外,p1 等於 p2。 因為編譯器會根據編譯時間類型來挑選 IEquatableTwoDPoint 實作,所以會忽略差異。

record 類型的內建實值相等會正確處理這類情節。 如果 TwoDPointThreeDPointrecord 類型,則 p1.Equals(p2) 的結果會是 False。 如需詳細資訊,請參閱 record 類型繼承階層中的相等

結構範例

下列範例示範如何在結構 (實值型別) 中實作實值相等:

namespace ValueEqualityStruct
{
    struct TwoDPoint : IEquatable<TwoDPoint>
    {
        public int X { get; private set; }
        public int Y { get; private set; }

        public TwoDPoint(int x, int y)
            : this()
        {
            if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
            {
                throw new ArgumentException("Point must be in range 1 - 2000");
            }
            X = x;
            Y = y;
        }

        public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);

        public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;

        public override int GetHashCode() => (X, Y).GetHashCode();

        public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);

        public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
    }

    class Program
    {
        static void Main(string[] args)
        {
            TwoDPoint pointA = new TwoDPoint(3, 4);
            TwoDPoint pointB = new TwoDPoint(3, 4);
            int i = 5;

            // True:
            Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));
            // True:
            Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
            // True:
            Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
            // False:
            Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
            // False:
            Console.WriteLine("(pointA == null) = {0}", pointA == null);
            // True:
            Console.WriteLine("(pointA != null) = {0}", pointA != null);
            // False:
            Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
            // CS0019:
            // Console.WriteLine("pointA == i = {0}", pointA == i);

            // Compare unboxed to boxed.
            System.Collections.ArrayList list = new System.Collections.ArrayList();
            list.Add(new TwoDPoint(3, 4));
            // True:
            Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));

            // Compare nullable to nullable and to non-nullable.
            TwoDPoint? pointC = null;
            TwoDPoint? pointD = null;
            // False:
            Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);
            // True:
            Console.WriteLine("pointC == pointD = {0}", pointC == pointD);

            TwoDPoint temp = new TwoDPoint(3, 4);
            pointC = temp;
            // True:
            Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);

            pointD = temp;
            // True:
            Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);

            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }

    /* Output:
        pointA.Equals(pointB) = True
        pointA == pointB = True
        Object.Equals(pointA, pointB) = True
        pointA.Equals(null) = False
        (pointA == null) = False
        (pointA != null) = True
        pointA.Equals(i) = False
        pointE.Equals(list[0]): True
        pointA == (pointC = null) = False
        pointC == pointD = True
        pointA == (pointC = 3,4) = True
        pointD == (pointC = 3,4) = True
    */
}

若為結構,預設實作 Object.Equals(Object) (這是 System.ValueType 中的覆寫版本) 會使用反映執行實值相等檢查,比較類型中的每個欄位值。 實作器覆寫結構中的虛擬 Equals 方法時,其目的是為了提供更有效率的方法來執行實值相等檢查,以及選擇性地根據結構的一部分欄位或屬性進行比較。

除非結構明確多載 ==!= 運算子,否則這些運算子無法在結構上運作。

另請參閱