HOW TO:定義型別的實值相等 (C# 程式設計手冊)
定義類別或結構時,需判斷是否有必要為型別建立實值相等 (或等價) 的自訂定義。 通常,當該型別的物件應加入至某種集合,或物件的主要目的是用來儲存一組欄位或屬性時,就會實作實值相等。 您可以根據對該型別中所有欄位和屬性的比較來定義實值相等,或也可以根據子集來進行定義。 不論使用哪種定義方法,對類別或結構而言,您的實作都必須遵循下列五項等價保證:
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(null) 傳回 false。 不過,null.Equals(null) 會擲回例外狀況,此項不遵守上述第二條規則。
您定義的結構已具有實值相等的預設實作,這是從 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,則請勿採用此做法,因為 Object.Equals(Object) 的 Object 實作會執行參考相等檢查)。
覆寫 Object.GetHashCode,讓具有實值相等的兩個物件可以產生相同的雜湊程式碼。
選用:若要支援「大於」或「小於」的定義,請為型別實作 IComparable<T> 介面,並多載 <= 和 >= 運算子。
下列第一個範例顯示類別實作。 第二個範例則顯示結構實作。
範例
下列範例會顯示如何在類別 (參考型別) 內實作實值相等。
namespace ValueEquality
{
using System;
class TwoDPoint : IEquatable<TwoDPoint>
{
// Readonly auto-implemented properties.
public int X { get; private set; }
public int Y { get; private set; }
// Set the properties in the constructor.
public TwoDPoint(int x, int y)
{
if ((x < 1) || (x > 2000) || (y < 1) || (y > 2000))
throw new System.ArgumentException("Point must be in range 1 - 2000");
this.X = x;
this.Y = y;
}
public override bool Equals(object obj)
{
return this.Equals(obj as TwoDPoint);
}
public bool Equals(TwoDPoint p)
{
// If parameter is null, return false.
if (Object.ReferenceEquals(p, 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()
{
return X * 0x00010000 + Y;
}
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
// Check for null on left side.
if (Object.ReferenceEquals(lhs, null))
{
if (Object.ReferenceEquals(rhs, null))
{
// null == null = true.
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)
{
return !(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 System.ArgumentException("Point must be in range 1 - 2000");
this.Z = z;
}
public override bool Equals(object obj)
{
return this.Equals(obj as ThreeDPoint);
}
public bool Equals(ThreeDPoint p)
{
// If parameter is null, return false.
if (Object.ReferenceEquals(p, 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()
{
return (X * 0x100000) + (Y * 0x1000) + Z;
}
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
// Check for null.
if (Object.ReferenceEquals(lhs, null))
{
if (Object.ReferenceEquals(rhs, 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)
{
return !(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.
System.Console.WriteLine("Press any key to exit.");
System.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 方法,則也應該多載 == 和 != 運算子,但這不是必要步驟。
下列範例顯示如何在結構 (實值型別) 內實作實值相等:
struct TwoDPoint : IEquatable<TwoDPoint>
{
// Read/write auto-implemented properties.
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
X = x;
Y = x;
}
public override bool Equals(object obj)
{
if (obj is TwoDPoint)
{
return this.Equals((TwoDPoint)obj);
}
return false;
}
public bool Equals(TwoDPoint p)
{
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode()
{
return X ^ Y;
}
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
{
return !(lhs.Equals(rhs));
}
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// Compare using virtual Equals, static Equals, and == and != operators.
// 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("pointE.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);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.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 方法,其目的是要提供更有效的執行實值相等檢查方法,並可選擇根據結構之欄位或屬性的某個子集進行比較。
== 和 != 運算子不能在結構上操作,除非結構明確多載它們。