重写相等操作符是非常容易出错的。不仅因为相等操作符有许多内涵,而且目前有很多指导文档有瑕疵,甚至在 MSDN 网站上有些指导文档也有瑕疵。我们将分别对支持相等操作的引用类型和值类型给出系统的分析,来澄清事实。
为了清晰起见,这里将类称作引用类型而结构称作值类型。
通常在结构中操作符重载比在类中有意义,所以我们先来展示在结构中的情况。类和结构的主要区别是,类需要检查空值,而在结构中你需要意识到可能存在的类型装箱。这一点将在后面说明。
类签名
结构的签名是直接了当的。你仅仅需要用 System.IEquatable 接口来标识该结构。请注意这个接口没有非泛型的版本,泛型的版本的角色由基础类 Object 承担。
C#<br></br><span color="#2a00ff">struct</span> <span color="#3f7f7f">PointStruct</span> : System.<span color="#3f7f7f">IEquatable<pointstruct></pointstruct></span><p>VB</p><br></br><span color="#2a00ff">Public Structure</span> PointStruct<br></br><span color="#2a00ff">Implements</span> IEquatable(<span color="#2a00ff">Of</span> PointStruct)
类的签名本质上和结构签名是一样的。类的继承会破坏相等性,这会造成问题。如果 a 是一个基类而 b 是一个重写了 Equals 方法的子类,那么 a.Equals(b) 会返回与 b.Equals(a) 不同的返回值。后面我们通过封闭(sealing)的 Equals 方法来解决这个问题。
C#<br></br><span color="#2a00ff">class</span> <span color="#3f7f7f">PointClass</span> : System.<span color="#3f7f7f">IEquatable<pointclass></pointclass></span><p>VB</p><br></br><span color="#2a00ff">Public Class</span> PointClass <span color="#2a00ff">Implements</span> IEquatable(<span color="#2a00ff">Of</span> PointClass)
## 成员变量和属性
任何用于相等性比较的成员变量必须是不可变的。通常,这意味着类中所有的属性是只读的或者类有一个类似于数据库主键的唯一标识符。
在使用任何依赖哈希的东西的时候这条规则都是至关重要的。这样的例子包括 Hashtable、Dictionary、HashSet 和 KeyedCollection。这些类都使用哈希码作查找和存储。如果对象的哈希码变化了,它会被放在错误的槽中而且集合不能再正确的工作。最常见的故障是不能找到以前放在集合中的对象。
为了确保成员变量是不可变的,它们被标记为只读的。由于成员变量可以在构造器中设置,所以改变成员变量有点象写错名字。但是一旦初始化完成了,没有方法可以直接改变成员变量的值。
C#<br></br><span color="#2a00ff">readonly int</span> _X;<br></br><span color="#2a00ff">readonly int</span> _Y;<p><span color="#2a00ff">public</span> PointStruct (<span color="#2a00ff">int</span> x, <span color="#2a00ff">int</span> y)</p><br></br> {<br></br> _X = x;<br></br> _Y = y;<br></br> }<p><span color="#2a00ff">int</span> X</p><br></br> {<br></br><span color="#2a00ff">get</span> { <span color="#2a00ff">return</span> _X; }<br></br> }<p><span color="#2a00ff">int</span> Y</p><br></br> {<br></br><span color="#2a00ff">get</span> { <span color="#2a00ff">return</span> _Y; }<br></br> }<br></br>VB<br></br><span color="#2a00ff">Private ReadOnly</span> m_X <span color="#2a00ff">As Integer</span><br></br><span color="#2a00ff">Private ReadOnly</span> m_Y <span color="#2a00ff">As Integer</span><p><span color="#2a00ff">Public Sub New</span>(<span color="#2a00ff">ByVal</span> x <span color="#2a00ff">As Integer, ByVal</span> y <span color="#2a00ff">As Integer</span>)</p><br></br> m_X = x<br></br> m_Y = y<br></br><span color="#2a00ff">End Sub<p> Public ReadOnly Property</p></span> X() <span color="#2a00ff">As Integer</span><p><span color="#2a00ff">Get</span><span color="#2a00ff">Return</span> m_X</p><br></br><span color="#2a00ff">End Get<br></br> End Property<p> Public ReadOnly Property</p></span> Y() <span color="#2a00ff">As Integer</span><p><span color="#2a00ff">Get</span><span color="#2a00ff">Return</span> m_Y</p><br></br><span color="#2a00ff">End Get<br></br> End Property</span>
由于类版本的代码与上面的代码几乎是相同的,且在 VB 中是完全相同的,所以这里不给出类版本代码。
类型安全的相等方法
我们实现的第一个方法是类型安全的相等方法,在 IEquatable 接口中使用。
C#<br></br><span color="#2a00ff">public bool</span> Equals(<span color="#3f7f7f">PointStruct</span> other)<br></br> {<br></br><span color="#2a00ff">return</span> (<span color="#2a00ff">this</span>._X == other._X) && (<span color="#2a00ff">this</span>._Y == other._Y);<br></br> }<p>VB <span color="#2a00ff">Public Overloads Function</span> Equals(<span color="#2a00ff">ByVal</span> other <span color="#2a00ff">As</span> PointStruct) <span color="#2a00ff">As Boolean</span> _</p><br></br><span color="#2a00ff">Implements</span> System.IEquatable(<span color="#2a00ff">Of</span> PointStruct).Equals<br></br><span color="#2a00ff">Return</span> m_X = other.m_X <span color="#2a00ff">AndAlso</span> m_Y = other.m_Y<br></br><span color="#2a00ff">End Function</span>
对于类,需要额外检查空值。按照惯例,所有非空值被认为与空值不相等。
你会注意到我们没有使用地道的 C#代码来检查空值。这是由于 C#和 VB 处理相等性的方式有一处不同。
Visual Basic 在处理引用相等和值相等上有明确的区别。前者使用 Is 擦作符,后者使用 = 操作符。
C#缺乏这种区别,对两者都使用 == 操作符。由于我们会重写 == 操作符,所以不想使用 ==,不得不转而使用一个后门。这个后门是 Object.ReferenceEquals 方法。
由于类总是与自己相等,所以在进行潜在的更昂贵的相等性检查之前,我们首先作这个检查。在下面代码中我们比较了私有成员变量,也可以使用属性来作比较。
C#<br></br><span color="#2a00ff">public bool</span> Equals(<span color="#3f7f7f">PointClass</span> other)<br></br> {<br></br><span color="#2a00ff">if</span> (<span color="#3f7f7f">Object</span>.ReferenceEquals(other, <span color="#2a00ff">null</span>))<br></br> {<p><span color="#2a00ff">return false;</span> }</p><br></br><span color="#2a00ff">if</span> (<span color="#3f7f7f">Object</span>.ReferenceEquals(other, <span color="#2a00ff">this</span>))<br></br> {<p><span color="#2a00ff">return true;</span> }</p><br></br><span color="#2a00ff">return</span> (<span color="#2a00ff">this</span>._X == other._X) && (this._Y == other._Y);<br></br> }<p>VB</p><br></br><span color="#2a00ff">Public Overloads Function</span> Equals(<span color="#2a00ff">ByVal</span> other <span color="#2a00ff">As</span> PointClass) <span color="#2a00ff">As Boolean<br></br> Implements</span> System.IEquatable(Of PointClass).Equals<br></br><span color="#2a00ff">If</span> other <span color="#2a00ff">Is Nothing Then Return False</span><br></br><span color="#2a00ff">If</span> other <span color="#2a00ff">Is Me Then Return True</span><br></br><span color="#2a00ff">Return</span> m_X = other.m_X <span color="#2a00ff">AndAlso</span> m_Y = other.m_Y<br></br><span color="#2a00ff">End Function</span>
## 哈希码
下一步是产生哈希码。最简单的方法是将所有用于相等性比较的成员变量的哈希码作异或运算。
C#<br></br><span color="#2a00ff">public override</span> int GetHashCode()<br></br> {<br></br><span color="#2a00ff">return</span> _X.GetHashCode() ^ _Y.GetHashCode();<br></br> }<p>VB</p><br></br><span color="#2a00ff">Public Overrides Function</span> GetHashCode() <span color="#2a00ff">As Integer</span><br></br><span color="#2a00ff">Return</span> m_X.GetHashCode <span color="#2a00ff">Xor</span> m_Y.GetHashCode<br></br><span color="#2a00ff">End Function</span>
如果你确实决定要从头写自己的哈希码,你必须确保对于一套给定的值,你总是能返回相同的哈希码。换言之,如果 a 等于 b,那么它们的哈希码也相等。
哈希码不必是唯一的,不同的值可以有相同的哈希码。但是它们应该有一个良好的分布。对于每一个哈希码都返回 42 在技术上是合法的,但是任何使用该算法的应用在性能上都会很糟糕。
哈希码应该以非常快的速度计算出来。由于计算哈希值可能成为瓶颈,所以宁可选用一个快速的有合理良好分布的哈希码算法,而不选择一个慢的,复杂的有着完美的均匀分布的算法。
相等(对象)
重写基类的 Equals 方法是一个基础工作,该方法被 Object.Equals(Object, Object) 函数和其他方法调用。
你应该注意到由于类型转换做了两次,所以可能存在一点性能问题:一次是看它是否有效,第二次是真正的执行它。不幸的是,在结构中是无法避免这样作的。
C#<br></br><span color="#2a00ff">public override bool</span> Equals(<span color="#2a00ff">object</span> obj)<br></br> {<br></br><span color="#2a00ff">if</span> (obj <span color="#2a00ff">is</span> <span color="#3f7f7f">PointStruct</span>)<br></br> {<br></br><span color="#2a00ff">return this</span>.Equals((<span color="#3f7f7f">PointStruct</span>)obj);<br></br> }<br></br><span color="#2a00ff">return false</span>;<br></br> }<p>VB</p><br></br><span color="#2a00ff">Public Overrides Function</span> Equals(<span color="#2a00ff">ByVal</span> obj <span color="#2a00ff">As Object) As Boolean</span><br></br><span color="#2a00ff">If TypeOf</span> obj <span color="#2a00ff">Is</span> PointStruct <span color="#2a00ff">Then Return CType</span>(obj, PointStruct) = <span color="#2a00ff">Me<br></br> End Function</span>
对于类可以只用一次类型转换。在处理步骤中,我们可以早点检测空值然后跳过对 Equals(PointClass) 方法的调用。C#必须用 ReferenceEquals 函数来检查空值。
为了防止子类破坏相等性,我们封闭(Seal)了方法。
C#<br></br><span color="#2a00ff">public sealed override bool</span> Equals(<span color="#2a00ff">object</span> obj)<br></br> {<br></br><span color="#2a00ff">var</span> temp = obj <span color="#2a00ff">as</span> <span color="#3f7f7f">PointClass</span>;<br></br><span color="#2a00ff">if</span> (!<span color="#3f7f7f">Object</span>.ReferenceEquals(temp, <span color="#2a00ff">null</span>))<br></br> {<br></br><span color="#2a00ff">return this</span>.Equals(temp);<br></br> }<br></br><span color="#2a00ff">return false</span>;<br></br> }<p>VB</p><br></br><span color="#2a00ff">Public NotOverridable Overrides Function</span> Equals(<span color="#2a00ff">ByVal</span> obj <span color="#2a00ff">As Object</span>) <span color="#2a00ff">As Boolean</span><br></br><span color="#2a00ff">Dim</span> temp = <span color="#2a00ff">TryCast</span>(obj, PointClass)<br></br><span color="#2a00ff">If</span> temp <span color="#2a00ff">IsNot Nothing Then Return Me</span>.Equals(temp)<br></br><span color="#2a00ff">End Function</span>
## 操作符重载
所有的难题都被攻克了,我们现在可以进行操作符的重写了。这里和调用类型安全的 Equlas 方法一样简单。
C#<br></br><span color="#2a00ff">public static bool operator</span> ==(<span color="#3f7f7f">PointStruct</span> point1<span color="#3f7f7f"> PointStruct</span> point2)<br></br> {<br></br><span color="#2a00ff">return</span> point1.Equals(point2);<br></br> }<p><span color="#2a00ff">public static bool operator</span> !=(<span color="#3f7f7f">PointStruct</span> point1, <span color="#3f7f7f">PointStruct</span> point2)</p><br></br> {<br></br><span color="#2a00ff">return</span> !(point1 == point2);<br></br> }<br></br>VB<br></br><span color="#2a00ff">Public Shared Operator</span> =(<span color="#2a00ff">ByVal</span> point1 <span color="#2a00ff">As</span> PointStruct, ByVal point2 As PointStruct) <span color="#2a00ff">As Boolean</span><br></br><span color="#2a00ff">Return</span> point1.Equals(point2)<p><span color="#2a00ff">End Operator</span><span color="#2a00ff">Public Shared Operator</span> <>(<span color="#2a00ff">ByVal</span> point1 <span color="#2a00ff">As</span> PointStruct, </p><br></br><span color="#2a00ff"> ByVal</span> point2 As PointStruct) <span color="#2a00ff">As Boolean</span><br></br><span color="#2a00ff">Return Not</span> (point1 = point2)<br></br><span color="#2a00ff">End Operator</span>
对于类,需要检查空值。幸运的是,Object.Equals(object, object) 为你处理了这种情况。然后调用已经被重写的 Object.Equals(Object) 方法。
C#<br></br><span color="#2a00ff">public static bool operator</span> ==(<span color="#3f7f7f">PointClass</span> point1, <span color="#3f7f7f">PointClass</span> point2)<br></br> {<br></br><span color="#2a00ff">return</span> Object.Equals(point1, point2);<br></br> }<p><span color="#2a00ff">public static bool operator</span> !=(<span color="#3f7f7f">PointClass</span> point1, <span color="#3f7f7f">PointClass</span> point2)</p><br></br> {<br></br><span color="#2a00ff">return</span> !(point1 == point2);<br></br> }<p>VB</p><br></br><span color="#2a00ff">Public Shared Operator</span> =(<span color="#2a00ff">ByVal</span> point1 <span color="#2a00ff">As</span> PointClass, <span color="#2a00ff">ByVal</span> point2 <span color="#2a00ff">As</span> PointClass) <span color="#2a00ff">As Boolean</span><br></br><span color="#2a00ff">Return Object</span>.Equals(point1 ,point2)<p><span color="#2a00ff">End Operator</span><span color="#2a00ff">Public Shared Operator</span> <>(<span color="#2a00ff">ByVal</span> point1 <span color="#2a00ff">As</span> PointClass, <span color="#2a00ff">ByVal</span> point2 <span color="#2a00ff">As</span> PointClass) <span color="#2a00ff">As Boolean</span></p><br></br><span color="#2a00ff">Return Not</span> (point1 = point2)<br></br><span color="#2a00ff">End Operator</span>
## 性能
你会注意到每个调用链都有点长,尤其是不相等操作符。如果需要考虑性能问题,你可以分别在每个方法中实现比较逻辑来提高速度。这样很容易出错而且使得维护工作比较辣手,所以仅仅当你使用性能检测工具证明了必须这样作之后,才应该这样作。
测试
本文使用了下面的测试。它使用了列表的形式使得你可以方便的将它们翻译到你最喜欢的单元测试框架中。因为相等性很容易被破坏,所以这是单元测试的首要的测试目标。
请注意测试不是全面的。你应该测试左右值部分相等的情况,例如 PointStruct(1, 2) and PointStruct(1, 5)。
Variable Type Value A PointStruct new PointStruct(1, 2) a2 PointStruct new PointStruct(1, 2) B PointStruct new PointStruct(3, 4) nullValue Object Null
Expression Expected Value Equal values
a == a2 True a != a2 False a.Equals(a2) True object.Equals(a, a2) True Unequal values, a on left
b == a False b != a True b.Equals(a) False object.Equals(b, a) False Unequal values, a on right
a == b False a != b True a.Equals(b) False object.Equals(a, b) False nulls, a on left
a.Equals(nullValue) False object.Equals(a, nullValue) False nulls, a on right
object.Equals(nullValue, a) False Hash codes
a.GetHashCode() == a2.GetHashCode() True a.GetHashCode() == b.GetHashCode() Indeterminate Variable Type Value a PointClass new PointClass (1, 2) a2 PointClass new PointClass (1, 2) b PointClass new PointClass (3,4) nullValue PointClass Null nullValue2 PointClass Null
Expression Expected Value Same Object a == a True a != a False a.Equals(a) True object.Equals(a, a) True Equal values
a == a2 True a != a2 False a.Equals(a2) True object.Equals(a, a2) True Unequal values, a on left
b == a False b != a True b.Equals(a) False object.Equals(b, a) False Unequal values, a on right
a == b False a != b True a.Equals(b) False object.Equals(a, b) False nulls, a on left
a == nullValue False a != nullValue True a.Equals(nullValue) False object.Equals(a, nullValue) False nulls, a on right
nullValue == a False nullValue != a True object.Equals(nullValue, a) False both null
nullValue == nullValue2 True object.Equals(nullValue, nullValue2) True Hash codes
a.GetHashCode() == a2.GetHashCode() True a.GetHashCode() == b.GetHashCode() Indeterminate阅读英文原文: A Detailed look at Overriding the Equality Operator
评论