在C#中,相等性分为两种:一是引用相等性,即两个引用指向同一个对象;二是值相等性,即两个值在某种角度上来说是相等的。
相等性比较的途径有:一是 object 类的静态方法 object.ReferenceEquals;二是 object 类的虚拟实例方法 object.Equals 和静态方法 Object.Equals。三是 IEquatable
本系列分为三部分。本文是第一部分,介绍 各种相等性比较的基本概念和方法。
object.ReferenceEquals 强制执行引用相等性比较,因此向其传入的参数应为引用类型。如果传入值类型,则会执行装箱操作后再比较,结果总是 False。
var obj1 = new object();
var obj2 = new object();
var obj3 = obj1;
// 输出: False
Console.WriteLine(object.ReferenceEquals(obj1, obj2));
// 输出:True
Console.WriteLine(object.ReferenceEquals(obj1, obj3));上例中,obj1 和 obj2 不是同一个对象,因此用 object.ReferenceEquals 比较的结果是 False。而 test3 通过赋值和 test1 指向了同一对象,因此相等。
为什么要专门定义一个强制做引用相等性比较的静态方法呢,这是因为无论是 == 运算符还是 object 的实例及静态比较方法,其行为都是可以通过重载改变的,后面我们会介绍。
object 类定义了一个虚拟的实例方法 object.Equals,签名如下。
public virtual bool Equals(Object? obj);既然是虚方法,那么其行为在运行时根据实际类型决定。
object x = 10;
object y = 20;
object z = 10;
// 输出:False
Console.WriteLine(x.Equals(y));
// 输出:True
Console.WriteLine(x.Equals(z));上述代码实际执行的是 int 类型的 Equals 方法。值类型总是执行值相等性,引用类型默认执行的是引用相等性,但可以通过重载改变。比如,匿名类和记录(Record) 都对 Equals 方法进行了重写,执行值相等性比较。还有一些特殊类,特别是仅仅包含少量数据的类,可能也重写了 Equals 方法以执行值相等性比较,最常见的比如 Uri 类。
实例类型的 Equals 方法有个缺陷,就是无法比较两个 null 值,因为在 null 上调用 Equals 方法会引发异常。为了解决此问题,object 类还提供了一个静态帮助类,名称同样是 Equals,签名和实现方法如下。
public static bool Equals(Object? objA, Object? objB);
{
if (obj1 == null)
return obj2 == null;
return obj1.Equals (obj2);
}object.Equals(Object? objA, Object? objB) 有个不足之处,就是如果传入的参数如果是值类型,会强制进行装箱操作,影响程序运行性能。对此,从 C# 2.0 起,又引入了 IEquatable
public interface IEquatable
{
bool Equals(T? other);
} 绝大多数 .Net 基本类型的实现了 IEquatable
// 输出: False
Console.WriteLine(1.Equals(2));以上代码调用的是 IEquatable 接口的 public bool Equals(Int32 obj),而不是object 的对象的 public override bool Equals(object? obj),无需装箱操作。
和 object.Equals 一样,默认情况下,== 运算符对引用类型执行的是引用相等性比较,对值类型执行的值相等性比较,这与 object.Equals 是一样的。
int x = 1;
int y = 2;
// 输出 : False
Console.WriteLine( x == y)以上代码,因为 int 是值类型,因此默认执行值相等性比较。
var test1 = new Test() { X = 1 };
var test2 = new Test() { X = 2 };
var test3 = test1;
// 输出: False
Console.WriteLine(test1 == test2);
// 输出:True
Console.WriteLine(test1 == test3);以上代码中,因为 Test 类是引用类型,所以默认执行的是引用相等性比较。可以通过运算符重载改变这种行为,我们在第三部分介绍。
既然 == 和 object.Equals 默认行为是一样的,而且也都可以通过重载改变行为,那么为什么搞得这么复杂,直接保留一个 == 不是更简单吗?
其实是这样的,在 C# 中,运算符是静态解析的。也就是说,对于 == 运算符,C# 在编译阶段就决定了要执行哪种操作。而 object.Equals 是实例的虚方法,需要在运行时才能确定实际要运行的代码。
== 的静态性带来的好处有两个方面:一是在第一个操作数为 null 的情况下也依然可用,object.Equals 则不行。二是因为是静态解析的,所以执行速度更快,这对于相等性比较这种在编程中使用非常高频的操作尤为关键。
同时,在某些情况下,让 == 和 Equals 有不一样的结果是有意义的。比如,让 == 执行引用相等性比较,Equals 执行值相等性比较。如果重写以改变比较行为,我们在第三部分介绍。
比如,在浮点数中,有一个非数 NaN(Not a Number),从数学意义上来说,两个 NaN 是不相等的。因此 float 和 double 重写了 == 运算符,让 NaN 和 NaN 不相等。但在约定中, object.Equals 方法必须保证自己等于自己,因此两者的行为出现了不一致。
float x = float.NaN;
// 输出:False
Console.WriteLine(x == x);
// 输出:True
Console.WriteLine(x.Equals(x)); | 留言与评论(共有 0 条评论) “” |