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