8.4 比较对象的相等性
在讨论了运算符并简要介绍了相等运算符后,就应考虑在处理类和结构的实例时,“相等”意味着什么。理解对象相等的机制对逻辑表达式的编程非常重要,另外对实现运算符重载和类型强制转换也非常重要,本章后面将讨论运算符重载。
对象相等的机制有所不同,这取决于比较的是引用类型(类的实例)还是值类型(基本数据类型、结构或枚举的实例)。下面分别介绍引用类型和值类型的相等性。
8.4.1 比较引用类型的相等性
System.Object定义了3个不同的方法来比较对象的相等性:ReferenceEquals()和两个版本的Equals()。再加上比较运算符(==),实际上有4种比较相等性的方法。这些方法有一些细微的区别,下面就介绍它们。
1. ReferenceEquals()方法
ReferenceEquals()是一个静态方法,其测试两个引用是否指向类的同一个实例,特别是两个引用是否包含内存中的相同地址。作为静态方法,它不能重写,所以System.Object的实现代码保持不变。如果提供的两个引用指向同一个对象实例,则ReferenceEquals()总是返回true;否则就返回false。但是,它认为null等于null:
SomeClass x, y; x = new SomeClass(); y = new SomeClass(); bool B1 = ReferenceEquals(null, null); // returns true bool B2 = ReferenceEquals(null, x); // returns false bool B3 = ReferenceEquals(x, y); // returns false because x and y // point to different objects
2. Equals()虚方法
Equals()虚版本的System.Object实现代码也可以比较引用。但因为这是虚方法,所以可以在自己的类中重写它,从而按值来比较对象。特别是如果希望类的实例用作字典中的键,就需要重写这个方法,以比较相关值。否则,根据重写Object.GetHashCode()的方式,包含对象的字典类要么不工作,要么工作的效率非常低。在重写Equals()方法时要注意,重写的代码不应抛出异常。同理,这是因为如果抛出异常,字典类就会出问题,一些在内部调用这个方法的.NET基类也可能出问题。
3.静态的Equals()方法
Equals()的静态版本与其虚实例版本的作用相同,其区别是静态版本带有两个参数,并对它们进行相等性比较。这个方法可以处理两个对象中有一个是null的情况;因此,如果一个对象可能是null,这个方法就可以抛出异常,提供额外的保护。静态重载版本首先要检查传递给它的引用是否为null。如果它们都是null,就返回true(因为null与null相等)。如果只有一个引用是null,它就返回false。如果两个引用实际上引用了某个对象,它就调用Equals()的虚实例版本。这表示在重写Equals()的实例版本时,其效果相当于也重写了静态版本。
4.比较运算符(==)
最好将比较运算符看作严格的值比较和严格的引用比较之间的中间选项。在大多数情况下,下面的代码表示正在比较引用:
bool b = (x == y); // x, y object references
但是,如果把一些类看作值,其含义就会比较直观,这是可以接受的方法。在这些情况下,最好重写比较运算符,以执行值的比较。后面将讨论运算符的重载,但一个明显例子是System.String类,Microsoft重写了这个运算符,以比较字符串的内容,而不是比较它们的引用。
8.4.2 比较值类型的相等性
在比较值类型的相等性时,采用与引用类型相同的规则:ReferenceEquals()用于比较引用,Equals()用于比较值,比较运算符可以看作一个中间项。但最大的区别是值类型需要装箱,才能把它们转换为引用,进而才能对它们执行方法。另外,Microsoft已经在System.ValueType类中重载了实例方法Equals(),以便对值类型进行合适的相等性测试。如果调用sA.Equals(sB),其中sA和sB是某个结构的实例,则根据sA和sB是否在其所有的字段中包含相同的值而返回true或false。另一方面,在默认情况下,不能对自己的结构重载“==”运算符。在表达式中使用(sA == sB)会导致一个编译错误,除非在代码中为存在问题的结构提供了“==”的重载版本。
另外,ReferenceEquals()在应用于值类型时总是返回false,因为为了调用这个方法,值类型需要装箱到对象中。即使编写下面的代码:
bool b = ReferenceEquals(v, v); // v is a variable of some value type
也会返回false,因为在转换每个参数时,v都会被单独装箱,这意味着会得到不同的引用。出于上述原因,调用ReferenceEquals()来比较值类型实际上没有什么意义,所以不能调用它。
尽管System.ValueType提供的Equals()默认重写版本肯定足以应付绝大多数自定义的结构,但仍可以针对自己的结构再次重写它,以提高性能。另外,如果值类型包含作为字段的引用类型,就需要重写Equals(),以便为这些字段提供合适的语义,因为Equals()的默认重写版本仅比较它们的地址。