什么时候==会以不同的方式覆盖.equals?

我理解==和.equals之间的区别。 这里还有很多其他问题可以解释细节上的差异,例如这一个: .Equals和==这一点之间的区别是什么 :其他许多人之间的比特 平等 。

我的问题是:为什么他们两个(我意识到必须有一个非常好的理由) – 他们似乎都做同样的事情(除非被覆盖不同)。

什么时候==会以不同的方式重载.equals被覆盖的方式?

我的问题是:为什么他们两个(我意识到必须有一个很好的理由)

如果有充分的理由还没有向我解释。 C#中的平等比较是一个混乱的混乱,并且在我对C#的设计感到遗憾的事情列表中排名第9:

http://www.informit.com/articles/article.aspx?p=2425867

在数学上,相等是最简单的等价关系,它应遵守规则: x == x应始终为真, x == y应始终与y == xx == yx != y应始终相同如果x == yy == z为真,那么x == z必须为真。 C#的==Equals机制不保证这些属性! (尽管如此,谢天谢地, ReferenceEquals保证所有这些。)

正如Jon在他的回答中所说,基于两个操作数的编译时类型调度== ,并且根据左操作数的运行时类型调度来自IEquatable .Equals(object).Equals(T) 。 。 为什么这些调度机制中的任何一个都正确? 平等不是一个有利于左手的谓词,那么为什么有些但不是所有的实现都这样做呢?

实际上我们想要的用户定义的相等是一种多方法,其中两个操作数的运行时类型具有相同的权重,但这不是C#中存在的概念。

更糟糕的是, Equals==被赋予不同的语义是非常常见的 – 通常一个是引用相等而另一个是值相等。 天真的开发者没有理由知道哪个是哪个,或者它们是不同的。 这是一个相当大的错误来源。 当你意识到GetHashCode和Equals必须同意时,它会变得更糟,但==不需要。

我是从头开始设计一种新语言,而我出于某种疯狂的原因希望运算符重载 – 我不这样做 – 那么我会设计一个更加直接的系统。 类似于:如果你在一个类型上实现IComparable ,那么你会自动获得<<===!=等等,为你定义的运算符,并且它们被实现以便它们是一致的。 即x<=y必须具有x的语义 x以及!(x>y)的语义,并且x == y始终与y == x相同,依此类推。

现在,如果您的问题确实是:

我们到底是怎么进入这个神圣的混乱?

然后我在2009年写下了一些想法:

https://blogs.msdn.microsoft.com/ericlippert/2009/04/09/double-your-dispatch-double-your-fun/

TLDR是:框架设计者和语言设计者有不同的目标和不同的约束,他们有时不会在设计中考虑这些因素,以确保整个平台的一致,合理的体验。 这是设计过程的失败。

什么时候==会以不同的方式重载.equals被覆盖的方式?

除非我有一个非常不寻常的,非常好的理由,否则我绝不会这样做。 当我实现算术类型时,我总是将所有运算符实现为彼此一致。

==在编译时静态绑定,因为运算符始终是静态的。 你重载运算符 – 你不能覆盖它们。 Equals(object)以多态方式执行,因为它被覆盖了。

就你什么时候想要它们而言……

引用类型通常会覆盖Equals但不会重载== 。 容易区分“这两个引用引用相同的对象”和“这两个引用引用相同的对象”之间的区别可能很有用。 (当然,如果有必要,你可以使用ReferenceEquals正如Eric在评论中指出的那样,这一点更清楚。)你想要清楚知道你何时这样做,请注意。

double对NaN值有此行为; 当任一操作数为NaN==(double, double)将始终返回false,即使它们是相同的NaN。 如果不使合同无效,则Equals不能这样做。 ( 不可否认, GetHashCode因不同的NaN值而被破坏,但这是另一回事 ……)

我个人不记得曾经实施过它们以给出不同的结果。

可能出现的一种情况是,当您拥有依赖于引用相等性的先前代码库时,通过== ,但您决定要添加值相等性检查。 一种方法是实现IEquatable ,这很好,但现在假设只有引用的所有现有代码都相同? inheritance的Object.Equals应该与IEquatable.Equals工作方式不同吗? 这并不是一个简单的答案,因为理想情况下,您希望所有这些函数/运算符以一致的方式运行。

对于发生这种情况的BCL中的具体情况,请查看TimeZoneInfo 。 在那种特殊情况下, ==Object.Equals保持不变,但这不是明确的,这是最好的选择。


顺便说一句,可以缓解上述问题的一种方法是使类不可变。 在这种情况下,代码不太可能因先前依赖引用相等而被破坏,因为您不能通过引用改变实例并使先前检查的相等性无效。

通常,您希望他们做同样的事情,特别是如果您的代码将由除您自己以外的任何人和您旁边的人使用。 理想情况下,对于使用您的代码的任何人,您都希望遵循最小意外的原则,这种原则会随机违反行为。 说完这个:

重载相等通常是一个坏主意,除非类型是不可变的,并且是密封的。 如果你正处于你不得不提出问题的阶段,那么在任何其他情况下纠正它的可能性都很小。 这有很多原因:

A. Equals和GetHashCode一起使用以使字典和散列集工作 – 如果您的实现不一致(或者散列代码随时间变化),则可能出现以下情况之一:

  • 字典/集合开始执行有效的线性时间查找。
  • 项目在词典/集中丢失

B.你真的想做什么? 通常,面向对象语言中对象的身份是它的参考。 因此,拥有两个具有不同引用的相同对象只会浪费内存。 首先可能没有必要创建副本。

C.当您开始实现对象的相等性时,您经常发现的是您正在寻找“为特定目的”而定义的相等性。 这使得为​​此目的烧掉你的唯一Equals是一个非常糟糕的主意 – 更好地为使用定义不同的EqualityComparers。

D.正如其他人指出的那样,你重载运算符但覆盖方法。 这意味着除非操作符调用方法,否则当有人试图使用==并且发现错误的(意外的)方法在层次结构的错误级别被调用时,会发生非常有趣和不一致的结果。