小编典典

为什么我们必须在 C# 中同时定义 == 和 !=?

all

C# 编译器要求,每当自定义类型定义
operator==时,它也必须定义!=(参见此处)。

为什么?

我很想知道为什么设计者认为有必要,以及为什么编译器不能在只有另一个运算符存在的情况下默认为其中一个运算符提供合理的实现。例如,Lua
允许您只定义相等运算符,而您可以免费获得另一个。C# 可以通过要求您定义 == 或同时定义 == 和 != 来做同样的事情,然后自动将缺少的 !=
运算符编译为!(left == right).

我知道存在一些奇怪的极端情况,其中某些实体可能既不相等也不不相等(例如 IEEE-754 NaN),但这些似乎是例外,而不是规则。所以这并不能解释为什么
C# 编译器设计者将例外作为规则。

我见过定义相等运算符的工艺不佳的情况,然后不相等运算符是复制粘贴,每个比较都反转,每个 && 切换到 || (你明白了......基本上!(a ==
b)通过德摩根的规则扩展)。这是编译器可以通过设计消除的不良做法,就像 Lua 一样。

注意:运算符 < > <= >= 也是如此。我无法想象您需要以不自然的方式定义这些的情况。Lua 允许您仅定义 < 和 <= 并通过前者的否定自然地定义

= 和 >。为什么 C# 不做同样的事情(至少“默认”)?

编辑

显然,有正当的理由允许程序员按照他们喜欢的方式实现相等和不相等的检查。一些答案指出了这可能很好的情况。

然而,我的问题的核心是为什么在 C# 中强制要求这 通常在 逻辑上 没有必要?

它也与 .NET 接口等的设计选择形成鲜明对比Object.EqualsIEquatable.Equals
IEqualityComparer.Equals其中缺少NotEquals对应项表明框架将!Equals()对象视为不平等,仅此而已。此外,类Dictionary和方法类.Contains()完全依赖于上述接口,即使定义了运算符,也不会直接使用它们。事实上,当
ReSharper
生成相等成员时,它同时定义了==!=Equals()即使这样,也仅当用户选择生成运算符时。框架不需要相等运算符来理解对象相等。

基本上,.NET 框架并不关心这些运算符,它只关心几个Equals方法。要求用户同时定义 == 和 != 运算符的决定纯粹与语言设计有关,而就 .NET
而言与对象语义无关。


阅读 94

收藏
2022-03-31

共1个答案

小编典典

我不能代表语言设计者说话,但据我所知,这似乎是有意的、正确的设计决定。

查看这个基本的 F# 代码,您可以将其编译成一个工作库。这是 F# 的合法代码,仅重载相等运算符,不重载不等式:

module Module1

type Foo() =
    let mutable myInternalValue = 0
    member this.Prop
        with get () = myInternalValue
        and set (value) = myInternalValue <- value

    static member op_Equality (left : Foo, right : Foo) = left.Prop = right.Prop
    //static member op_Inequality (left : Foo, right : Foo) = left.Prop <> right.Prop

这正是它的样子。它只创建一个相等比较器==,并检查类的内部值是否相等。

虽然不能在 C# 中创建这样的类,但 可以 使用为 .NET 编译的类。很明显它会使用我们重载的操作符== 那么,运行时是用来做什么的!=呢?

C# EMCA 标准有一大堆规则(第 14.9 节)解释如何确定在评估相等时使用哪个运算符。简而言之,如果被比较的类型是相同的类型 并且
存在重载的相等运算符,那么它将使用该重载而不是继承自 Object
的标准引用相等运算符。因此,如果只有一个运算符存在,它将使用所有对象都有的默认引用相等运算符也就不足为奇了,它没有重载。1

知道是这种情况,真正的问题是:为什么要这样设计,为什么编译器不自己解决?很多人说这不是一个设计决定,但我喜欢这样想,特别是考虑到所有对象都有一个默认的相等运算符这一事实。

那么,为什么编译器不自动创建!=运算符呢?除非微软的人证实了这一点,否则我无法确定,但这是我根据事实推理可以确定的。


防止意外行为

也许我想做一个值比较==来测试相等性。但是,当涉及到!=值是否相等时,除非引用相等,否则我根本不在乎,因为我的程序认为它们相等,我只关心引用是否匹配。毕竟,这实际上被概述为
C# 的默认行为(如果两个运算符都没有重载,就像某些用另一种语言编写的 .net
库的情况一样)。如果编译器自动添加代码,我就不能再依赖编译器来输出应该符合要求的代码。编译器不应该编写改变你行为的隐藏代码,尤其是当你编写的代码符合 C#
和 CLI 的标准时。

就它 强迫 你重载它而言,而不是去默认行为,我只能坚定地说它在标准中 (EMC-334 17.9.2) 2。该标准没有说明原因。我相信这是因为 C#
借鉴了 C++ 的许多行为。有关更多信息,请参见下文。


当您覆盖!=and==时,您不必返回 bool。

这是另一个可能的原因。在 C# 中,此函数:

public static int operator ==(MyClass a, MyClass b) { return 0; }

和这个一样有效:

public static bool operator ==(MyClass a, MyClass b) { return true; }

如果您返回的不是 bool,编译器 就不能 自动推断出相反的类型。此外,在您的操作员 确实 返回 bool
的情况下,他们创建仅存在于一种特定情况下的生成代码或者如我上面所说的隐藏 CLR 的默认行为的代码是没有意义的。


C# 借鉴了 C++ 3

在介绍 C# 的时候,MSDN 杂志上有一篇文章写到,谈到 C#:

许多开发人员希望有一种像 Visual Basic 一样易于编写、阅读和维护的语言,但它仍然提供了 C++ 的强大功能和灵活性。

是的,C# 的设计目标是提供与 C 几乎相同的功能,只牺牲一点点来实现严格的类型安全和垃圾收集等便利。C# 强烈模仿 C

得知在 C++ 中, 相等运算符不必返回 bool
,您可能不会感到惊讶,如本示例程序所示

现在,C++ 并不直接 要求 您重载互补运算符。如果您在示例程序中编译了代码,您将看到它运行时没有错误。但是,如果您尝试添加该行:

cout << (a != b);

你会得到

编译器错误 C2678 (MSVC) : binary ‘!=’ : no operator found 接受’Test’
类型的左操作数(或没有可接受的转换)`。

因此,虽然 C 本身不需要您成对重载,但它 不会 让您使用尚未在自定义类上重载的相等运算符。它在 .NET
中有效,因为所有对象都有一个默认值;C
没有。


1. 作为旁注,如果您想重载其中一个运算符,C# 标准仍然要求您重载这对运算符。这是 标准 的一部分,而不仅仅是 编译器
。但是,当您访问用另一种语言编写的、没有相同要求的 .net 库时,有关确定调用哪个运算符的相同规则也适用。

2. EMCA-334 (pdf) ( http://www.ecma-
international.org/publications/files/ECMA-ST/Ecma-334.pdf )

3. 还有Java,但这真的不是重点

2022-03-31