最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

深入探察相等操作符

  • 2008-06-02
  • 本文字数:7250 字

    阅读完需:约 24 分钟

重写相等操作符是非常容易出错的。不仅因为相等操作符有许多内涵,而且目前有很多指导文档有瑕疵,甚至在 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

2008-06-02 23:051529
用户头像

发布了 47 篇内容, 共 99592 次阅读, 收获喜欢 3 次。

关注

评论

发布
暂无评论
发现更多内容

架构师第十一周作业及总结

傻傻的帅

游戏夜读 | 什么才值得纪念?

game1night

安全系列之——主流Hash散列算法介绍和使用

诸葛小猿

hash 散列函数 md5 sha1 murmurhash

分手快乐 祝你快乐 你可以找到更好的

escray

学习 面试

oeasy教您玩转linux010105详细手册man

o

架构师训练营第十一周作业

张明森

微服务编程范式

看山

微服务 范式 签约计划第二季

论商品促销代码的优雅性

架构师修行之路

week11 作业

Geek_196d0f

有益思考一则:框架性思维

石君

学习 方法论

沟通是一门艺术

石云升

情绪控制 沟通艺术

一起学MySQL性能优化

xcbeyond

MySQL 性能优化 MySQL性能优化

架构师训练营第十一周总结

张明森

架构师训练营第11周作业

Bruce Xiong

薪水真的不是工作的全部

escray

学习 面试

ArCall 升级丨新增多项功能,可支持多人在线语音

anyRTC开发者

音视频 WebRTC 直播 RTC

第二周学习总结

Vincent

极客时间 极客大学 作业

微服务的基建工作

看山

微服务 基础设施 签约计划第二季

Apache 软件基金会顶级项目 Pulsar 达成新里程碑:全球贡献者超 300 位!

Apache Pulsar

Apache Apache Pulsar 消息系统 消息中间件

Flink状态管理-8

小知识点

大数据 flink scal

跨过语言银河,构筑智能鹊桥:百度NLP的十年、今夕与未来

脑极体

Spring系列篇:Spring容器基本使用及原理

简爱W

开源流数据公司 StreamNative 推出 Pulsar 云服务,推进企业“流优先”进程

Apache Pulsar

Apache Pulsar 消息系统 消息中间件

大数据技术思想入门(五):分布式计算特点

cristal

Java 大数据 hadoop 分布式

如何在面试中表现你所没有的能力

escray

学习 面试

第二周作业

Vincent

极客时间 作业

架构师训练营 -- 第11周作业

stardust20

week11 小结

Geek_196d0f

性能相关,进程调度

Linuxer

计算机网络基础(二十一)---传输层-TCP连接的四次挥手

书旅

TCP 四次挥手 TCP/IP 协议族

满足消费者仪式感要求,木莲庄酒店做得很到位

InfoQ_967a83c6d0d7

深入探察相等操作符_.NET_Jonathan Allen_InfoQ精选文章