一尘不染

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

c#

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

为什么?

我很好奇,为什么设计师会认为这是必要的,以及为什么当仅存在另一个运算符时,编译器为何不能默认其中一个运算符为合理的实现。例如,Lua允许您仅定义相等运算符,而另一个则免费。C#可以通过要求您定义==或同时定义==和!=,然后将缺少的!=运算符自动编译为来执行相同的操作!(left == right)

我知道有些情况下有些实体可能不相等或不相等(例如IEEE-754
NaN),但是在某些特殊情况下,这似乎是个例外,而不是规则。因此,这不能解释为什么C#编译器设计人员将例外作为规则。

我已经看到了定义平等运算符的工艺不佳的情况,然后不平等运算符是一个复制粘贴,每个比较都相反,每个&&切换为||。(您明白了……基本上!(a ==
b)通过De Morgan的规则扩展了)。与Lua的情况一样,编译器可以通过设计消除这种糟糕的做法。

注意:运算符<> <=> =也是如此。我无法想象需要用不自然的方式定义它们的情况。Lua允许您仅定义<和<=,并通过前者的否定自然定义>
=和>。C#为什么不做同样的事情(至少是“默认”)?

编辑

显然,有充分的理由允许程序员执行他们喜欢的相等性和不平等性检查。一些答案指出了可能不错的情况。

但是,我的问题的核心是,为什么在C#中 通常逻辑上 不必要时强制执行此操作?

这与.NET接口(如)的设计选择形成了鲜明的对比Object.EqualsIEquatable.Equals
IEqualityComparer.Equals后者缺少NotEquals对应项表明该框架将!Equals()对象视为不平等,仅此而已。此外,类Dictionary和方法之类的类.Contains()仅取决于上述接口,即使定义了它们也不直接使用运算符。实际上,当ReSharper生成相等成员时,它仅在用户选择完全生成运算符的情况下才定义,==以及!=Equals()并以此为依据。框架不需要相等运算符来了解对象相等。

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


阅读 194

收藏
2020-05-19

共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个

知道是这种情况,真正的问题是:为什么这样设计,为什么编译器不自行解决?很多人说这不是设计决定,但我想认为是这样设计的,尤其是在所有对象都有默认的相等运算符的情况下。

那么,为什么编译器不能自动创建!=运算符呢?除非有Microsoft的确认,否则我无法确定,但这是我根据事实推理所能确定的。


防止意外行为

也许我想做一个值比较==来测试相等性。但是,涉及到!=我完全不在乎值是否相等,除非引用相等,因为我的程序认为它们相等,所以我只在乎引用是否匹配。毕竟,这实际上被概述为C#的默认行为(如果两个运算符都未重载,例如某些.net库用另一种语言编写的情况)。如果编译器是自动添加代码,那么我将不再依赖编译器来输出应该兼容的代码。编译器不应编写会改变您的行为的隐藏代码,尤其是当您编写的代码在C#和CLI的标准之内时。

关于它 迫使 您重载它,而不是采用默认行为,我只能坚定地说它是标准(EMCA-334 17.9.2)2。该标准未指定原因。我相信这是由于C#从C
++借用了许多行为这一事实。有关更多信息,请参见下文。


当您覆盖!=和时==,您不必返回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):二进制’!=’:找不到使用“测试”类型的左操作数(或没有可接受的转换)的运算符。

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


1.作为附带说明,如果要重载两个运算符,C#标准仍然要求您重载一对运算符。这是 标准 的一部分,而不仅仅是 编译器
。但是,当您访问以另一种要求不相同的语言编写的.net库时,将采用与确定调用哪个运算符相同的规则。

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

3.和Java,但这不是重点

2020-05-19