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

Ste*_*nev 341 c# language-design

C#编译器要求每当自定义类型定义运算符时==,它还必须定义!=(参见此处).

为什么?

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

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

我已经看到了定义等式运算符的做工不好的情况,然后不等式运算符是一个复制粘贴,每个比较都被反转,每个&&切换到|| (你得到的重点......基本上!(a == b)通过De Morgan的规则扩展).编译器可以通过设计消除这种糟糕的做法,就像Lua的情况一样.

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

编辑

显然有正当理由允许程序员实现对他们喜欢的平等和不平等的检查.一些答案指向可能很好的情况.

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

它与.NET接口的设计选择形成鲜明对比,例如Object.Equals,IEquatable.Equals IEqualityComparer.Equals缺少NotEquals对应物表明框架将!Equals()对象视为不相等而且就是这样.此外,类Dictionary和类等方法.Contains()完全依赖于上述接口,即使定义了运算符也不直接使用运算符.事实上,当ReSharper生成相等成员时,它定义了两者==并且!=就其而言,Equals()即使只是用户选择生成运算符.框架不需要相等运算符来理解对象相等性.

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

Chr*_*ens 158

我不能代表语言设计师,但从我的理由来看,这似乎是有意的,适当的设计决定.

查看这个基本的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
Run Code Online (Sandbox Code Playgroud)

这完全是它的样子.它仅创建一个相等比较器==,并检查该类的内部值是否相等.

虽然您无法在C#中创建这样的类,但您可以使用为.NET编译的类.很明显它将使用我们的重载运算符,== 那么运行时使用了!=什么?

C#EMCA标准有一大堆规则(第14.9节),解释了如何确定在评估相等性时使用哪个运算符.为了使它过于简化而不是完全准确,如果要比较的类型是相同类型并且存在重载的相等运算符,它将使用该重载而不是从Object继承的标准引用相等运算符.因此,如果只有一个运算符存在,它将使用所有对象都具有的默认引用相等运算符,并不会出现过载,这一点也就不足为奇了.1

知道情况就是这样,真正的问题是:为什么这样设计,为什么编译器不能自己解决?很多人都说这不是一个设计决策,但是我觉得它是这样想的,特别是关于所有对象都有一个默认的相等运算符.

那么,为什么编译器不能自动创建!=运算符呢?除非微软的某人确认这一点,否则我无法确定,但这是我可以通过推理事实来确定的.


防止意外行为

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

强迫你超载它而言,我只能坚定地说它符合标准(EMCA-334 17.9.2)2,而不是采用默认行为.该标准没有说明原因.我相信这是因为C#从C++中借用了很多行为.有关详细信息,请参见下文.


当你重写!===,你不必返回布尔.

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

public static int operator ==(MyClass a, MyClass b) { return 0; }
Run Code Online (Sandbox Code Playgroud)

和这个一样有效:

public static bool operator ==(MyClass a, MyClass b) { return true; }
Run Code Online (Sandbox Code Playgroud)

如果你返回bool以外的东西,编译器不能自动推断出相反的类型.此外,在您的运算符确实返回bool 的情况下,它们只是没有意义创建生成代码,这些代码只存在于该特定情况下,或者如上所述,代码隐藏了CLR的默认行为.


C#借用了C++ 3

当引入C#时,MSDN杂志上有一篇文章写了一篇关于C#的文章:

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

是的,C#的设计目标是提供与C++几乎相同的功率,为了便利而牺牲一点,例如刚性类型安全和垃圾收集.C#在C++之后强烈建模.

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

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

cout << (a != b);
Run Code Online (Sandbox Code Playgroud)

你会得到

编译器错误C2678(MSVC):binary'!=':找不到运算符,它接受类型为'Test'的左手操作数(或者没有可接受的转换)`.

因此,虽然C++本身并不要求您成对重载,但它不允许您使用未在自定义类上重载的相等运算符.它在.NET中有效,因为所有对象都有默认对象; C++没有.


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

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

和Java一样,但这不是重点

  • "**你不必返回布尔**"......我认为这就是我们的答案; 做得好.如果!(o1 == o2)甚至没有明确定义,那么你不能指望标准依赖它. (72认同)
  • @Dan Diplo,没关系,这并不是说我不禁止不这样做. (12认同)
  • 具有讽刺意味的是,在一个关于逻辑运算符的主题中,你在句子中使用了双重否定,"C#不允许你只覆盖其中一个",这实际上逻辑上意味着"C#允许你只覆盖其中一个". (10认同)
  • 如果是真的,我怀疑#2是正确的; 但现在问题变成了,为什么允许`==`返回除了`bool`以外的任何东西?如果返回`int`,则不能再将其用作条件语句,例如.`if(a == b)`将不再编译.重点是什么? (6认同)
  • 你可以返回一个隐式转换为bool的对象(operator true/false),但我仍然看不到哪个有用. (2认同)
  • @Danny,如果你想实现一些非布尔逻辑,你可以返回除bool以外的东西.例如,在某些模糊逻辑场景中,您不确定某个值是否等于另一个值,因此您可以返回值等于另一个值的*probability*(0到1之间的double值) . (2认同)
  • @carlosfigueira,但是你不会为了这个目的而使用带有描述性名称的方法,而不是可能会产生混淆的操作员吗? (2认同)
  • 啊,所以你的意思是一个糟糕的选择导致另一个.第一个糟糕的选择是拥有不返回布尔值的谓词.这导致我们需要的糟糕选择覆盖`==`和`!=`. (2认同)
  • @Greg H,作为示例用例,使用`==`或`!=`返回除布尔值之外的其他内容允许传染性错误结果.例如,在IEE754中,"0.0/0.0"是一个特殊的"NaN"值,它会感染所有算术运算符; `NaN + anyValue`是`NaN`,类似于`-`,`*`和`/`.但是,当比较值时,该错误位经常会无意中丢失.通过这种重载,您可以为逻辑运算符执行相同类型的传染性错误值. (2认同)

Yuc*_*uck 50

可能是因为有人需要实现三值逻辑(即null).在这种情况下 - 例如ANSI标准SQL - 根据输入的不同,不能简单地否定运算符.

你可能有一个案例:

var a = SomeObject();
Run Code Online (Sandbox Code Playgroud)

a == true返回falsea == false返回false.

  • 如果`a == true`是'false`那么我总是希望`a!= true`为'true`,尽管`a == false`为`false` (27认同)
  • @Fede击中头部.**这真的与问题**无关 - 你在解释为什么有人可能想要覆盖`==`,而不是为什么他们应该被迫覆盖两者. (19认同)
  • 顺便说一句,我不敢相信没有人引用[FileNotFound](http://thedailywtf.com/Articles/What_Is_Truth_0x3f_.aspx)这个笑话. (18认同)
  • @Yuck - 是的,有一个很好的默认实现:这是常见的情况.即使在你的例子中,`!=`的默认实现也是正确的.在*极其罕见的情况下,我们希望`x == y`和`x!= y`都是假的*(我想不出一个有意义的例子)*,它们仍然可以覆盖默认实现并提供自己的.*始终将常见情况作为默认情况!* (5认同)
  • 与SQL比较好. (2认同)

Bri*_*don 24

除了C#在很多方面都遵循C++之外,我能想到的最好的解释是,在某些情况下,你可能想采用一种稍微不同的方法来证明"不平等"而不是证明"平等".

显然,通过字符串比较,return当您看到不匹配的字符时,您可以只测试相等性和循环外.但是,它可能不是那么干净,有更复杂的问题.在布隆过滤器想到的; 它很容易迅速告诉我们,如果元素是不是在集,但很难说,如果该元素在集合.虽然return可以应用相同的技术,但代码可能不那么漂亮.

  • 所以这解释了为什么他们应该被赋予*选项*来覆盖两者,而不是为什么他们应该*强迫*. (13认同)
  • 很好的例子; 我想你是因为这个原因.一个简单的`!`将你锁定为一个单一的相等定义,一个知情的编码器可能能够优化**'=='和`!=`. (6认同)

hat*_*ica 20

如果你在.net源代码中查看==和!=的重载实现,它们通常不会实现!= as!(left == right).他们使用否定逻辑完全实现它(如==).例如,DateTime实现== as

return d1.InternalTicks == d2.InternalTicks;
Run Code Online (Sandbox Code Playgroud)

和!= as

return d1.InternalTicks != d2.InternalTicks;
Run Code Online (Sandbox Code Playgroud)

如果您(或编译器是否隐式执行)将实现!= as

return !(d1==d2);
Run Code Online (Sandbox Code Playgroud)

然后你在你的类引用的东西中假设==和!=的内部实现.避免这种假设可能是他们决定背后的哲学.


Kei*_*thS 16

要回答你的编辑,关于为什么你被强制覆盖两者如果你覆盖一个,它都在继承中.

如果你重写==,最有可能提供某种语义或结构相等性(例如,如果他们的InternalTicks属性相等,即使它们可能是不同的实例,则DateTimes相等),那么你正在改变运算符的默认行为Object,它是所有.NET对象的父级.在C#中,==运算符是一种方法,其基本实现Object.operator(==)执行参照比较.Object.operator(!=)是另一种不同的方法,它也执行参考比较.

在几乎任何其他方法覆盖的情况下,假设覆盖一种方法也会导致对反义方法的行为改变是不合逻辑的.如果您使用Increment()和Decrement()方法创建了一个类,并在子类中覆盖了Increment(),您是否还希望使用与被覆盖的行为相反的方式覆盖Decrement()?在所有可能的情况下,编译器都不能足够智能地为操作符的任何实现生成反函数.

然而,运营商虽然实施方式与方法非常相似,但在概念上成对运作; ==和!=,<和>,以及<=和> =.从消费者的角度来看,在这种情况下认为!=与==的工作方式不同,这是不合逻辑的.因此,在所有情况下都不能让编译器假设a = = b ==!(a == b),但通常期望==和!=应该以类似的方式运行,因此编译器强制你要成对实施,但实际上你最终会这样做.如果,对于你的类,a!= b ==!(a == b),那么只需使用!(==)实现!=运算符,但是如果该规则在所有情况下都不适用于您的对象(例如,如果与特定值(相等或不相等)进行比较无效,那么您必须比IDE更聪明.

应该问的真实问题是为什么<和>和<=和> =是比较运算符的对,必须同时实现,当用数字表示时!(a <b)== a> = b和!(a> b)== a <= b.你应该被要求实现所有四个,如果你重写一个,你应该被要求覆盖==(和!=),因为(a <= b)==(a == b)如果a是语义的等于b.


Chr*_*ins 13

如果你为自定义类型重载==,而不是!=那么它将由!=运算符处理对象!=对象,因为一切都是从对象派生的,这与CustomType!= CustomType有很大的不同.

此外,语言创建者可能希望以这种方式为编码人员提供最大的灵活性,并且他们也不会对您打算做什么做出假设.

  • 灵活性原因也可以应用于他们决定遗漏的许多功能,例如http://blogs.msdn.com/b/ericlippert/archive/2011/06/23/ref-returns-and-ref -locals.aspx.因此它没有解释他们对等于运算符的实现的选择. (2认同)

Dan*_*mov 9

这是我首先想到的:

  • 如果测试不平等比测试平等要快得多怎么办?
  • 如果在某些情况下,有什么要返回false两个==!=(即,如果它们不能出于某种原因进行比较)

  • 在第一种情况下,您可以在不平等测试方面实施相等性测试. (5认同)
  • @Stefan:`Dictionary`使用`GetHashCode`和`Equals`,而不是运算符.`List`使用`Equals`. (2认同)

Gol*_*rol 5

嗯,这可能只是设计选择,但正如您所说,x!= y不必与相同!(x == y)。通过不添加默认实现,可以确保您不会忘记实现特定实现。而且,如果确实如您所说的那样琐碎,则可以仅使用另一个实现。我看不出这是“不良做法”。

C#和Lua之间可能还有其他差异...

  • 那是程序员的问题,而不是C#的问题。未能实现此权利的程序员也将在其他领域失败。 (3认同)

Gre*_*ott 5

你问题中的关键词是" 为什么 "和" 必须 ".

结果是:

以这种方式回答它是因为他们设计得如此,这是真的......但没有回答你问题的"为什么"部分.

回答它有时可能有助于独立地覆盖这两者,这是真的......但不回答你问题的"必须"部分.

我认为简单的答案是,没有任何令人信服的理由为什么C#要求你覆盖它们.

应该让你只覆盖语言==,并为您提供的默认实现!=就是!这一点.如果你碰巧想要覆盖!=,那就去吧.

这不是一个好的决定.人类设计语言,人类并不完美,C#并不完美.耸肩和QED


And*_*ell 5

只是为了添加这里的优秀答案:

考虑一下调试器中会发生什么,当您尝试进入一个!=操作符并最终进入一个操作==符时!谈论混乱!

CLR 允许您自由地忽略一个或其他运算符是有道理的 - 因为它必须适用于多种语言。但也有很多的C#不暴露CLR功能(例子ref回报和当地人,例如),以及大量的未实现在CLR自身特点的例子(如:usinglockforeach,等)。