小提示
請先考慮使用 記錄 。 記錄會以最少的程式碼自動實作值相等,使其成為大多數以資料為中心的類型的建議方法。 如果您需要自訂值相等邏輯或無法使用記錄,請繼續執行下列手動實作步驟。
當您定義類別或結構時,需判斷是否有必要為類型建立實值相等 (或等價) 的自訂定義。 通常,如果您預期將該類型的物件新增至集合,或物件的主要目的是要儲存一組欄位或屬性,則會實作實值相等。 您可以根據對該類型中所有欄位和屬性的比較來定義實值相等,也可以根據子集來進行定義。
不論是哪一種情況,以及在類別和結構中,您的實作都應該遵循五個等價保證 (針對下列規則,假設 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)就會傳回相同的值。任何非空值的值不會等於空值。 不過,
x.Equals(y)為 Null 時,x會擲回例外狀況。 根據Equals的引數,這會破壞規則 1 或 2。
任何您已定義的結構,皆已擁有從 System.ValueType 方法的 Object.Equals(Object) 覆寫繼承而來的預設值相等性實作。 此實作使用反映來檢查類型中的所有欄位和屬性。 雖然此實作會產生正確的結果,但相較於您針對該類型特別撰寫的自訂實作卻慢得多。
對類別和結構而言,實值相等的實作細節並不同。 不過,類別和結構都需要相同的基本步驟來實作相等:
重寫虛擬Object.Equals(Object)方法。 這提供了多態相等行為,允許您的物件在被視為參考時
object正確比較。 它可確保集合中以及使用多態性時的正確行為。 在大部分情況下,實作bool Equals( object obj )應該只會呼叫特定類型的Equals方法,這是 System.IEquatable<T> 介面的實作。 (請參閱步驟 2)。透過提供類型專屬的 System.IEquatable<T> 方法實作
Equals介面。 這提供了類型安全的相等性檢查,無需裝箱,從而獲得更好的效能。 它還可以避免不必要的轉換,並啟用編譯時類型檢查。 實際的等價比較是在這裡執行。 例如,您可能決定只比較類型中的一個或兩個欄位,以定義相等。 不要從Equals擲回例外狀況。 針對透過繼承關聯的類別:此方法只會檢查在類別中宣告的欄位。 它應該呼叫
base.Equals以檢查基底類別中的欄位 (如果類型直接繼承自base.Equals,則請不要呼叫 Object,因為 Object 的 Object.Equals(Object) 實作會執行參考相等檢查。)只有在所比較變數的執行階段類型相同時,才應該將兩個變數視為相等。 此外,如果變數的執行階段和編譯時間類型不同,則請確定使用執行階段類型
IEquatable方法的Equals實作。 確保執行階段類型始終正確比較的其中一個方法,就是僅在IEquatable類別中實作sealed。 如需詳細資訊,請參閱本文稍後的類別範例。
選用但為建議動作︰多載 == 和 != 運算子。 這為相等比較提供了一致且直覺的語法,符合使用者對內建類型的期望。 它確保了這一點
obj1 == obj2並obj1.Equals(obj2)以相同的方式行事。覆寫 Object.GetHashCode,以便有實值相等的兩個物件產生相同的雜湊碼。 這是基於雜湊的集合(例如 和
Dictionary<TKey,TValue>)HashSet<T>中正確行為所必需的。 相等的物件必須具有相等的雜湊碼,否則這些集合將無法正常運作。選用︰若要支援「大於」或「小於」的定義,請為類型實作 IComparable<T> 介面,並同時多載 <= 和 >= 運算子。 這啟用了排序操作,並為您的類型提供了完整的排序關係,這在將物件新增至排序集合或排序陣列或清單時非常有用。
記錄範例
下列範例顯示記錄如何使用最少的程式碼自動實作值相等。 第一筆記錄 TwoDPoint 是自動實作值相等的簡單記錄類型。 第二筆記錄 ThreeDPoint 示範記錄可以衍生自其他記錄,且仍會維持適當的值相等行為:
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
記錄為價值平等提供了幾個優勢:
-
自動實作:記錄會自動實作 System.IEquatable<T> 並覆寫 Object.Equals、 Object.GetHashCode和
==/!=運算子。 -
正確的繼承行為:記錄使用虛擬方法來實作
IEquatable<T>,這些方法檢查兩個運算元的執行階段類型,確保繼承階層和多態性案例中的正確行為。 - 預設不變性:記錄鼓勵不可變設計,這與價值相等語意配合得很好。
- 簡潔語法:位置參數提供了一種定義資料類型的緊湊方法。
- 更好的效能:編譯器產生的相等實作經過最佳化,不會像預設結構實作那樣使用反射。
當您的主要目標是儲存資料且需要價值相等語意時,請使用記錄。
具有使用參照相等性成員的記錄
當記錄包含使用參考相等的成員時,記錄的自動值相等行為不會如預期般運作。 這適用於諸如 、 陣列和其他 System.Collections.Generic.List<T>未實現基於值相等的引用類型的集合(但 的顯著例外 System.String是 ,它確實實現了值相等)。
重要
雖然記錄為基本資料類型提供優異的值相等,但它們不會自動解決使用參考相等的成員的值相等。 如果記錄包含 System.Collections.Generic.List<T>、 System.Array或其他未實作值相等的參考類型,則這些成員中具有相同內容的兩個記錄實例仍然不相等,因為成員使用參考相等。
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
這是因為記錄會使用 Object.Equals 每個成員的方法,而集合類型通常會使用參照相等,而不是比較其內容。
以下顯示問題:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
以下是執行程式碼時的行為方式:
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
具有參照相等成員的記錄解決方案
自訂 System.IEquatable<T> 實作:將編譯器產生的相等式取代為手動編碼版本,為參考相等成員提供基於內容的比較。 對於集合,請使用或類似的方法實 Enumerable.SequenceEqual 作逐個元素的比較。
盡可能使用值類型:考慮您的資料是否可以用自然支援值相等的值類型或不可變結構來表示,例如 System.Numerics.Vector<T> 或 Plane。
使用具有基於值相等的類型:對於集合,請考慮使用實現基於值相等的類型,或實現覆蓋以提供基於內容的比較的 Object.Equals 自訂集合類型,例如 System.Collections.Immutable.ImmutableArray<T> 或 System.Collections.Immutable.ImmutableList<T>。
設計時要考慮參照相等性:接受某些成員將使用參照相等性並相應地設計您的應用程式邏輯,確保您在相等性很重要時重複使用相同的實例。
以下是為具有集合的記錄實作自訂相等的範例:
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
此自訂實作可正常運作:
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
同樣的問題會影響陣列和其他集合類型:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
陣列也使用引用相等,產生相同的意外結果:
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
即使是唯讀集合也會表現出這種參考相等行為:
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
關鍵見解是,記錄解決了 結構 相等問題,但不會改變它們所包含類型的 語義 相等行為。
類別範例
下列範例示範如何在類別 (參考型別) 中實作實值相等。 當您無法使用記錄或需要自訂相等邏輯時,需要此手動方法:
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。 因為編譯器會根據編譯時間類型來挑選 TwoDPoint 的 IEquatable 實作,所以會忽略差異。 這是繼承層次結構中多態相等的一個基本問題。
多態相等
在具有類別的繼承階層中實作值相等時,類別範例中所示的標準方法可能會導致物件多態使用時不正確的行為。 發生此問題的原因是 System.IEquatable<T> ,實作是根據編譯時間類型來選擇,而不是執行階段類型。
標準實作的問題
考慮這個有問題的場景:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
比較會傳回 True ,因為編譯器會根據宣告的類型進行選取 TwoDPoint.Equals(TwoDPoint) , Z 忽略座標差異。
修正多態相等的關鍵是確保所有相等比較都使用虛擬 Object.Equals 方法,該方法可以檢查運行時類型並正確處理繼承。 這可以透過使用委派至虛擬方法的 System.IEquatable<T> 明確介面實作來達成:
基類示範了關鍵模式:
// Safe polymorphic equality implementation using explicit interface implementation
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) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual 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) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
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);
}
以下是此實作如何處理有問題的多態性案例:
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
實作也會正確處理直接類型比較:
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
相等實作也適用於集合:
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
上述程式碼示範實作價值型相等的關鍵元素:
-
虛擬
Equals(object?)覆寫:主要相等邏輯發生在虛擬 Object.Equals 方法中,無論編譯時間類型為何,都會呼叫該方法。 -
運行時類型檢查:使用
this.GetType() != p.GetType()確保不同類型的物件永遠不會被視為相等。 - 顯式介面實作:實作委 System.IEquatable<T> 派給虛擬方法,防止編譯時類型選擇問題。
-
受保護的虛擬協助程式方法:該
protected virtual Equals(TwoDPoint? p)方法允許派生類別在保持類型安全的同時覆蓋相等邏輯。
在下列情況下使用此模式:
- 您有價值平等很重要的繼承層次結構
- 物件可能會以多態方式使用 (宣告為基底類型,具現化為衍生類型)
- 您需要具有值相等語意的參考類型
偏好的方法是使用類型來 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 方法時,目的是提供更有效率的方法來執行值相等性檢查,並選擇性地以結構欄位或屬性的某些子集為基礎進行比較。
除非結構明確多載 == 和 != 運算子,否則這些運算子無法在結構上運作。