写点什么

深入探察相等操作符

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

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

关注

评论

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

通义灵码内置 DeepSeek V3 和 R1 满血版 671B模型,免费不限量,免部署!

阿里巴巴云原生

阿里云 云原生 通义灵码 AI程序员

通义灵码内置 DeepSeek V3 和 R1 满血版 671B模型,免费不限量,免部署!

阿里云云效

阿里云 云原生 通义灵码 AI程序员

Python实现「手势猜拳游戏」:好玩的实时机器学习项目

知识浅谈

Python 人工智能 机器学习

AgentRunner:高性能任务调度器

FunTester

EMAS 性能分析全面适配HarmonyOS NEXT,开启原生应用性能优化新纪元

移动研发平台EMAS

性能优化 开发者工具 HarmonyOS NEXT EMAS性能分析 鸿蒙原生应用

加入Karmada用户组!连接全球同行共建多集群生态

华为云原生团队

云计算 容器 云原生

内购占比 45%、首日留存 50%,开发者揭秘热门手游《Trash Tycoon》成功秘籍

极客天地

人工智能丨使用实例:DeepSeek 在工作中的惊艳表现

测试人

人工智能

25年辽宁省等保测评机构新名单看这里!

行云管家

网络安全 等保 等保测评 辽宁

DeepSeek-R1 网页端稳定性测评:天工AI位居总榜第二

新消费日报

Qt 性能优化策略和技巧

北京木奇移动技术有限公司

软件外包公司 QT开发 QT外包公司

数字先锋 | 央企首批!天翼云助力中国石化率先完成全尺寸DeepSeek国产化部署!

天翼云开发者社区

人工智能 大模型 AI应用 DeepSeek

性能测试需求分析案例

老张

软件测试 性能测试 需求分析 质量保障

阿里云 MaxCompute MaxQA 开启公测,解锁近实时高效查询体验

阿里云大数据AI技术

大数据 数据分析 云原生 实时数仓 MaxCompute

不同的DNS解析记录分别代表什么含义

国科云

Qt开发框架及特点

北京木奇移动技术有限公司

软件外包公司 QT开发 QT外包公司

Qt 开发的性能测试

北京木奇移动技术有限公司

软件外包公司 QT开发 QT软件开发

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