写点什么

深入探察相等操作符

  • 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:051650
用户头像

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

关注

评论

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

专题 | IAM业界热度不减,2024市场持续井喷(二)

芯盾时代

身份安全 iam 身份和访问管理 零信任

苹果Mac图像修图软件Photomator和Pixelmator Pro 有什么区别?

Rose

【ACL2024】阿里云人工智能平台PAI多篇论文入选ACL2024

阿里云大数据AI技术

人工智能 阿里云 acl 论文 PAI

ps2019直装版:Photoshop 2019 for Mac/Win 版

你的猪会飞吗

Mac软件下载站 PS2019 Mac中文版下载 mac破解软件下载

苹果Mac电脑想要实现双系统运行,Parallels Desktop虚拟机来帮你!

Rose

Parallels Desktop 虚拟机 Mac双系统安装

奈飞中文影视_Netflix for mac v2.13.0激活版 支持m1/m2

理理

Clicker for Netflix Netflix客户端 Netflix mac版破解版下载 网飞客户端

Dopple Labs 选择 Zilliz Cloud 作为安全高效的向量数据库

Zilliz

人工智能 Milvus Zilliz 向量数据库 大语言模型

去中心化swap博饼交易所底池LP质押挖矿分红系统开发指南

V\TG【ch3nguang】

去中心化swap博饼交易所 LP质押挖矿分红系统开发

基于51单片机设计的公交车LED屏

DS小龙哥

8月月更

5款超好用的苹果电脑实用工具,提高Mac使用体验

Rose

面试这么多,为什么拿不到offer?

老张

面试 求职 职场成长

CSS萤火虫按钮特效

南城FE

CSS 前端 动画

全红婵夺冠!数业智能心大陆告诉你原生家庭在背后发挥了怎样的力量

心大陆多智能体

智能体 AI大模型 心理健康 数字心理

后期混音效果全套插件Waves 15插件下载安装(mac&win))

Rose

混音套件 Waves 15插件下载 Waves 15破解教程

苹果电脑如何修改hosts文件?mac修改Hosts文件教程

Rose

汉化版Microsoft Remote Desktop微软远程桌面使用教程

理理

好用的微软代码编辑器Visual Studio Code免费版(mac&win)

理理

Visual Studio Code 微软代码编辑器

如何卸载Maxon产品?红巨星系列插件如何彻底清除

Rose

红巨星激活系列插件

Advanced RAG 11:对用户输入的内容进行「分类处理」和「再优化」

Baihai IDP

AI LLMs 企业号 8 月 PK 榜 rag RQ-RAG

交易所/永续合约跟单交易系统开发稳定版/案例搭建/成熟技术

V\TG【ch3nguang】

开发者洞察报告:百万级鸿蒙岗位缺口,开发者薪资涨幅43.1%

最新动态

mac&win xmind思维导图破解安装包(附Xmind入门指南)

理理

XMind 2024破解版 XMind思维导图下载 xmind使用教程

面经精选:数据库高频面试十问

王中阳Go

数据库 面试

MES系统如何实现生产管理自动化

万界星空科技

mes 万界星空科技 制造业工厂 生产管理MES系统 自动化生产

无缝融入,即刻智能[一]:Dify-LLM大模型平台,零编码集成嵌入第三方系统,42K+星标见证专属智能方案[含ollama部署]

汀丶人工智能

人工智能 agent LLMOps rag dify

地理编码之旅,一场地址与坐标的漫游

HarmonyOS SDK

HarmonyOS

潜在新就业岗位超300万个 原生鸿蒙开发创造百万级人才缺口

最新动态

苹果Mac电脑怎么设置动态桌面,heic动态桌面壁纸怎么使用

Rose

永久使用 Photoshop CC 2019中文破解版下载安装包(mac&win)

理理

实力认证!望繁信科技入选2023WISE未来商业之王年度企业

望繁信科技

数字化转型 大模型 流程挖掘 流程智能 上海望繁信

中国开发者画像报告:鸿蒙开发人才缺口达百万,薪资与技术成长超预期

最新动态

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