共用方式為


如何定義類別或結構的實值相等 (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. 任何非空值的值不會等於空值。 不過,x.Equals(y) 為 Null 時,x 會擲回例外狀況。 根據 Equals 的引數,這會破壞規則 1 或 2。

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

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

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

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

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

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

  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) = {pointA.Equals(pointB)}");
        Console.WriteLine($"pointA == pointB = {pointA == pointB}");
        Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
        Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");

        TwoDPoint pointD = null;
        TwoDPoint pointE = null;

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

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

        System.Collections.ArrayList list = new System.Collections.ArrayList();
        list.Add(new ThreeDPoint(3, 4, 5));
        Console.WriteLine($"pointE.Equals(list[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

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

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) = {pointA.Equals(pointB)}");
            // True:
            Console.WriteLine($"pointA == pointB = {pointA == pointB}");
            // True:
            Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
            // False:
            Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
            // False:
            Console.WriteLine($"(pointA == null) = {pointA == null}");
            // True:
            Console.WriteLine($"(pointA != null) = {pointA != null}");
            // False:
            Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
            // CS0019:
            // Console.WriteLine($"pointA == i = {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]): {pointA.Equals(list[0])}");

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

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

            pointD = temp;
            // True:
            Console.WriteLine($"pointD == (pointC = 3,4) = {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 方法時,其目的是為了提供更有效率的方法來執行實值相等檢查,以及選擇性地根據結構的一部分欄位或屬性進行比較。

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

另請參閱