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

When would == be overridden in a different way to .equals?

我理解 == 和 .equals 之间的区别。这里还有很多其他问题可以详细解释差异,例如这个:What is the difference between .Equals and == this one: Bitwise equality 等等。

我的问题是:为什么要同时拥有它们(我意识到一定有一个很好的理由)- 它们似乎都在做同样的事情(除非以不同方式覆盖)。

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

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

就您希望它们何时有所不同而言...

引用类型通常会覆盖 Equals 但根本不会重载 ==。轻松区分 "these two references refer to the same object" 和 "these two references refer to equal objects" 可能很有用。 (当然,如果需要,您可以使用 ReferenceEquals - 正如 Eric 在评论中指出的那样,这更清楚。)您想要 真正 清楚何时执行此操作,请注意你.

double 对 NaN 值有这种行为;当任一操作数为 NaN 时,==(double, double) 将始终 return false,即使它们是相同的 NaN。 Equals 不能在不使其合同无效的情况下这样做。 (Admittedly GetHashCode is broken for different NaN values, but that's a different matter...)

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

可能出现的一种情况是,当您有一个以前的代码库依赖于通过 == 的引用相等性时,但您决定要添加值相等性检查。实现此目的的一种方法是实现 IEquatable<T>,这很好,但是现在所有假设仅引用相等的现有代码怎么办?继承的 Object.Equals 应该与 IEquatable<T>.Equals 的工作方式不同吗?这没有一个简单的答案,因为理想情况下,您希望所有这些 functions/operators 以一致的方式行事。

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


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

My question is: why have them both (I realise there must be a very good reason)

如果有充分的理由还没有向我解释。 C# 中的相等比较是一团糟,在我对 C# 设计感到遗憾的事项列表中排名第 9:

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

在数学上,相等是最简单的等价关系,它应该遵循以下规则:x == x应该永远为真,x == y应该永远与y == x相同,[=11] =] 和 x != y 应始终取相反值,如果 x == yy == z 为真,则 x == z 必须为真。 C#的==Equals机制保证了这些属性的none! (不过,谢天谢地,ReferenceEquals 保证了所有这些。)

正如乔恩在他的回答中指出的那样,== 是根据两个操作数的编译时类型分派的,而 IEquatable<T> 中的 .Equals(object).Equals(T) 是分派的基于左操作数的运行时类型。为什么这些调度机制 是正确的?相等不是偏向左侧的谓词,那么为什么一些而不是所有的实现都应该这样做?

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

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

如果我是从头开始设计一种新语言,并且出于某种疯狂的原因想要运算符重载——我没有——那么我会设计一个非常非常简单的系统。类似的东西:如果你在一个类型上实现 IComparable<T> 那么你会自动得到 <<===!= 等等,为你定义的运算符, 并且它们被实施以便它们是一致的。即 x<=y 必须具有 x<y || x==y 的语义,也必须具有 !(x>y) 的语义,并且 x == y 始终与 y == x 相同,等等。

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

How on earth did we get into this godawful mess?

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

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

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

When would == be overloaded in a different way to how .equals is overridden?

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

通常,您希望他们做同样的事情,特别是如果您的代码将被除您自己和旁边的人以外的任何人使用。理想情况下,对于使用您的代码的任何人,您都希望遵守最小惊奇原则,随机不同的行为违反了该原则。话虽如此:

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

一个。 Equals 和 GetHashCode 一起使用以使字典和哈希集工作 - 如果您的实现不一致(或者如果哈希码随时间变化),则可能会发生以下情况之一:

  • Dictionaries/sets 开始执行有效的线性时间查找。
  • 物品在 dictionaries/sets
  • 丢失

乙。你到底想做什么?通常,面向对象语言中对象的标识是它的引用。所以拥有两个具有不同引用的相同对象只是浪费内存。可能一开始就不需要创建副本。

C。当您开始为对象实现相等性时,您经常会发现您正在寻找一个相等性的定义 "for a particular purpose"。这使得为​​此燃烧唯一的 Equals 成为一个非常糟糕的主意 - 为用途定义不同的 EqualityComparer 更好。

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