如何定義類別或結構的實值相等 (C# 程式設計指南)
記錄會自動實作實值相等。 如果您的類型建立資料模型,而且應該實作實值相等,則請考慮定義 record
,而非 class
。
當您定義類別或結構時,需判斷是否有必要為類型建立實值相等 (或等價) 的自訂定義。 通常,如果您預期將該類型的物件新增至集合,或物件的主要目的是要儲存一組欄位或屬性,則會實作實值相等。 您可以根據對該類型中所有欄位和屬性的比較來定義實值相等,也可以根據子集來進行定義。
不論是哪一種情況,以及在類別和結構中,您的實作都應該遵循五個等價保證 (針對下列規則,假設 x
、y
和 z
不是 Null):
自反屬性:
x.Equals(x)
傳回true
。對稱屬性:
x.Equals(y)
傳回與y.Equals(x)
相同的值。可轉移的屬性:如果
(x.Equals(y) && y.Equals(z))
傳回true
,則x.Equals(z)
會傳回true
。只要 x 和 y 所參考的物件沒有經過修改,後續叫用
x.Equals(y)
就會傳回相同的值。任何非 Null 值不等於 Null。 不過,
x
為 Null 時,x.Equals(y)
會擲回例外狀況。 根據Equals
的引數,這會中斷規則 1 或 2。
任何您已定義的結構,皆有繼承自 Object.Equals(Object) 方法的 System.ValueType 覆寫的預設實作實值相等。 此實作使用反映來檢查類型中的所有欄位和屬性。 雖然此實作會產生正確的結果,但相較於您針對該類型特別撰寫的自訂實作卻慢得多。
對類別和結構而言,實值相等的實作細節並不同。 不過,類別和結構都需要相同的基本步驟來實作相等:
覆寫虛擬 Object.Equals(Object) 方法。 在大部分情況下,實作
bool Equals( object obj )
應該只會呼叫特定類型的Equals
方法,這是 System.IEquatable<T> 介面的實作。 (請參閱步驟 2)。透過提供類型專屬的
Equals
方法實作 System.IEquatable<T> 介面。 實際的等價比較是在這裡執行。 例如,您可能決定只比較類型中的一個或兩個欄位,以定義相等。 不會從Equals
擲回例外狀況。 針對繼承相關類別:此方法只會檢查在類別中宣告的欄位。 它應該呼叫
base.Equals
以檢查基底類別中的欄位 (如果類型直接繼承自 Object,則請不要呼叫base.Equals
,因為 Object.Equals(Object) 的 Object 實作會執行參考相等檢查。)只有在所比較變數的執行階段類型相同時,才應該將兩個變數視為相等。 此外,如果變數的執行階段和編譯時間類型不同,則請確定使用執行階段類型
Equals
方法的IEquatable
實作。 確定執行階段類型一律正確比較的一個策略,就是只在sealed
類別中實作IEquatable
。 如需詳細資訊,請參閱本文稍後的類別範例。
覆寫 Object.GetHashCode,以便有實值相等的兩個物件產生相同的雜湊碼。
選用︰若要支援「大於」或「小於」的定義,請為類型實作 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
。 因為編譯器會根據編譯時間類型來挑選 IEquatable
的 TwoDPoint
實作,所以會忽略差異。
record
類型的內建實值相等會正確處理這類情節。 如果 TwoDPoint
和 ThreeDPoint
是 record
類型,則 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
方法時,其目的是為了提供更有效率的方法來執行實值相等檢查,以及選擇性地根據結構的一部分欄位或屬性進行比較。
除非結構明確多載 == 和 != 運算子,否則這些運算子無法在結構上運作。