如何为类或结构定义值相等性(C# 编程指南)
记录自动实现值相等性。 当你的类型为数据建模并应实现值相等性时,请考虑定义 record
而不是 class
。
定义类或结构时,需确定为类型创建值相等性(或等效性)的自定义定义是否有意义。 通常,预期将类型的对象添加到集合时,或者这些对象主要用于存储一组字段或属性时,需实现值相等性。 可以基于类型中所有字段和属性的比较结果来定义值相等性,也可以基于子集进行定义。
在任何一种情况下,类和结构中的实现均应遵循 5 个等效性保证条件(对于以下规则,假设 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)
将引发异常。 这会违反规则 1 或 2,具体取决于Equals
的参数。
定义的任何结构都已具有其从 Object.Equals(Object) 方法的 System.ValueType 替代中继承的值相等性的默认实现。 此实现使用反射来检查类型中的所有字段和属性。 尽管此实现可生成正确的结果,但与专门为类型编写的自定义实现相比,它的速度相对较慢。
类和结构的值相等性的实现详细信息有所不同。 但是,类和结构都需要相同的基础步骤来实现相等性:
替代虚拟Object.Equals(Object) 方法。 大多数情况下,
bool Equals( object obj )
实现应只调入作为 System.IEquatable<T> 接口的实现的类型特定Equals
方法。 (请参阅步骤 2。)通过提供类型特定的
Equals
方法实现 System.IEquatable<T> 接口。 实际的等效性比较将在此接口中执行。 例如,可能决定通过仅比较类型中的一两个字段来定义相等性。 不会从Equals
引发异常。 对于与继承相关的类:此方法应仅检查类中声明的字段。 它应调用
base.Equals
来检查基类中的字段。 (如果类型直接从 Object 中继承,则不会调用base.Equals
,因为 Object.Equals(Object) 的 Object 实现会执行引用相等性检查。)仅当要比较的变量的运行时类型相同时,才应将两个变量视为相等。 此外,如果变量的运行时和编译时类型不同,请确保使用运行时类型的
Equals
方法的IEquatable
实现。 确保始终正确比较运行时类型的一种策略是仅在sealed
类中实现IEquatable
。 有关详细信息,请参阅本文后续部分的类示例。
替代 Object.GetHashCode,以便具有值相等性的两个对象生成相同的哈希代码。
可选:若要支持“大于”或“小于”定义,请为类型实现 IComparable<T> 接口,并同时重载 < 和 > 运算符。
注意
从 C# 9.0 开始,可以使用记录来获取值相等性语义,而不需要任何不必要的样板代码。
类示例
下面的示例演示如何在类(引用类型)中实现值相等性。
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
虚方法时,目的是提供更高效的方法来执行值相等性检查,并选择根据结构字段或属性的某个子集来进行比较。
除非结构显式重载了 == 和 != 运算符,否则这些运算符无法对结构进行运算。