写点什么

深入探察相等操作符

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

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

关注

评论

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

Hugging Face 的应用

霍格沃兹测试开发学社

阿里巴巴中国站1688商品搜索API返回值深度解析与实战应用

代码忍者

API 测试 API 策略

构建Web3社交平台:DeBox式DApp开发全攻略

区块链软件开发推广运营

交易所开发 dapp开发 链游开发 NFT开发 代币开发

ICE 8月各项板块盈利报告,人工智能与高频交易驱动稳定收益

科技热闻

《代码整洁之道》-大师眼中的整洁代码是什么样

京东零售技术

后端 代码

AI+医疗:营销机遇洞察2024

易观分析

医疗 人工智能’

避坑指南!细说Python自动化办公的5大缺点

程序员晚枫

Python 自动化办公

SyncBird Pro for Mac(iPhone文件管理器) v4.1.1激活版

Rose

收藏!打造高质量技术文章的8个技巧

京东零售技术

写作 企业号9月PK榜

阿里巴巴中国站商品搜索API返回值解析与实战

技术冰糖葫芦

api 网关 API Gateway API 测试 API 策略 pinduoduo API

精准学:用一根垂直大模型支柱,撑起教育普惠的未来

脑极体

AI

纯CSS实现海浪文字效果

南城FE

CSS 前端 动画

百年码头遇见数智化:华为和天津港解锁新质生产力

Alter

由点到面,英特尔与腾讯持续深耕云、网络、存储等领域

E科讯

等待风起——京东.Vision项目参与实录分享

京东零售技术

Vision pro 京东.vision

精准电商营销:基于京东商品详情API返回值的数据分析

技术冰糖葫芦

api 网关 API Gateway API 测试 API 策略 pinduoduo API

如何使用 StarRocks 管理和优化数据湖中的数据?

镜舟科技

数据湖 数据处理 分布式架构 StarRocks

京东鸿蒙上线前瞻——使用 Taro 打造高性能原生应用

京东零售技术

鸿蒙

开发者必备的 4 大类 AI 工具盘点,错过你就亏大了!

伤感汤姆布利柏

Java 内部类的使用场合介绍

秃头小帅oi

C++ 之 perf+火焰图分析与调试

芯动大师

perf C++ 火焰图

三分钟带你看懂,低代码开发赋能办公方式转变

不在线第一只蜗牛

低代码

Reflection 70B 遭质疑基模为 Llama 3;Replit Agent:编程 0 基础适用丨 RTE 开发者日报

声网

大奖收割机!望繁信科技荣获年度技术创新和应用品牌奖

望繁信科技

数字化转型 流程挖掘 流程资产 流程智能

从管控角度谈慢SQL治理

不思jo

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