经过对四种不同类型判等方法的讨论,我们不难发现不管是 Equals 静态方法、Equals 虚方法 抑或==操作符的执行结果,都可能受到覆写 Equals 方法的影响。因此研究对象判等就必须将注意 力集中在自定义类型中如何实现 Equals 方法,以及实现怎样的 Equals 方法。因为,不同的类型, 对于“相等”的理解会有所偏差,你甚至可以在自定义类型中实现一个总是相等的类型,例如:
class AlwaysEquals { public override bool Equals(object obj) { return true; } }
因此,Euqls 方法的执行结果取决于自定义类型的具体实现规则,而.NET 又为什么提供这种机 制来实现对象判等策略呢?首先,对象判等决定于需求,没有必要为所有.NET 类型完成逻辑判等, System.Object 基类也无法提供满足各种需求的判等方法;其次,对象判等包括值判等和引用判等 两个方面,不同的类型对判等的处理又有所不同,通过多态机制在派生类中处理各自的判等实现 显然是更加明智与可取的选择。
接下来,我们开始研究如何通过覆写 Equals 方法实现对象的判等。覆写 Equals 往往并非易事, 要综合考虑到对值类型字段和引用类型字段的分别判等处理,同时还要兼顾父类覆写所带来的影 响。不适当的覆写会引发意想不到的问题,所以必须遵循三个等价原则:自反、传递和对称,这 是实现 Equals 的通用契约。那么又如何为自定义类型实现 Equals 方法呢?
最好的参考资源当然来自于.NET 框架类库的实现,事实上,关于 Equals 的覆写在.NET 中已经 有很多的基本类型完成了这一实现。从值类型和引用类型两个角度来看:
对于值类型,基类 System.ValueType 通过反射机制覆写了 Equals 方法来比较两个对象的值相 等,但是这种方式并不高效,更明智的办法是在自定义值类型时有针对性的覆写 Equals 方法, 来提供更灵活、高效的处理机制。
对于引用类型,覆写 Equals 方法意味着要改变 System.Object 类型提供的引用相等语义。那么, 覆写 Equals 要根据类型本身的特点来实现,在.NET 框架类库中就有很多典型的引用类型实现 了值相等语义。例如 System.String 类型的两个变量相等意味着其包含了相等的内容,System. Version 类型的两个变量相等也意味着其 Version 信息的各个指标分别相等。
因此对 Equals 方法的覆写主要包括对值类型的覆写和对引用类型的覆写,同时也要区别基类 是否已经有过覆写和不曾覆写两种情况,并以等价原则为前提,进行判断。在此,我们仅提供较 为标准的实现方法,具体的实现取决于不同的类型定义和语义需求。
class EqualsEx { //定义值类型成员 ms private MyStruct ms; //定义引用类型成员 mc private MyClass mc; public override bool Equals(object obj) { //为 null,则必不相等 if (obj == null) return false; //引用判等为真,则二者必定相等 if (ReferenceEquals(this, obj)) return true; //类型判断 EqualsEx objEx = obj as EqualsEx; if (objEx == null) return false; //最后是成员判断,分值类型成员和引用类型成员 //通常可以提供强类型的判等方法来单独处理对各个成员的判等 return EqualsHelper(this, objEx); } private static bool EqualsHelper(EqualsEx objA, EqualsEx objB) { //值类型成员判断 if (!objA.ms.Equals(objA.ms)) return false; //引用类型成员判断 if (!Equals(objA.mc, objB.mc)) return false; //最后,才可以判定两个对象是相等的 return true; } } internal struct MyStruct { } internal class MyClass { }
上述示例只是从标准化的角度来阐释 Equals 覆写的简单实现,而实际应用时又会有所不同, 然而总结起来实现 Equals 方法我们应该着力于以下几点:首先,检测 obj 是否为 null,如果是则 必然不相等;然后,以 ReferenceEquals 来判等是否引用相等,这种办法比较高效,因为引用相等 即可以推出值相等;然后,再进行类型判断,不同类型的对象一定不相等;最后,也是最复杂的 一个过程,即对对象的各个成员进行比较,引用类型进行恒定性判断,值类型进行恒等性判断。 在本例中我们将成员判断封装为一个专门的处理方法 EqualsHelper,以隔离对类成员的判断实现, 主要有以下几个好处:
符合 Extract Method 原则,以隔离相对变化的操作。
提供了强类型版本的 Equals 实现,对于值类型成员来说还可以避免不必要的装箱操作。
为==操作符提供了重载实现的安全版本。
在.NET 框架中,System.String 类型的 Equals 覆写方法就提供了 EqualsHelper 方法来实现。