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


346

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而言,与对象语义无关。


24
+1是一个很好的问题。似乎根本没有任何原因...当然,除非另有说明,否则编译器可以假定!= b可以转换为!(a == b)。我很好奇他们为什么也决定这样做。
Patrick87

16
这是我所看到的投票赞成并被喜欢的问题中最快的。
Christopher Currens

26
我确定@Eric Lippert也会提供一个合理的答案。
Yuck

17
“科学家不是给出正确答案的人,而是提出正确问题的人”。这就是为什么在SE中鼓励提出问题的原因。而且有效!
Adriano Carneiro

4
Python的同一个问题:stackoverflow.com/questions/4969629/...
约什-李

Answers:


161

我不能代表语言设计师,但是从我可以推理的角度来看,这似乎是有意的,适当的设计决策。

查看此基本的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,但这不是重点


75
您不必归还布尔 ” ..我认为这就是我们在那里的答案;做得好。如果!(o1 == o2)甚至定义不明确,那么您就不能指望标准依赖它。
Brian Gordon

10
具有讽刺意味的是,在有关逻辑运算符的主题中,您在句子中使用了双负数,“ C#不允许您不覆盖任何一个”,这在逻辑上实际上意味着“ C#允许您仅覆盖任何一个”。
Dan Diplo

13
@丹·迪普洛(Dan Diplo),没关系,这不是不禁止我不这样做或不这样做。
克里斯托弗·柯伦斯

6
如果为真,我怀疑第二个是正确的。但是现在的问题变成了,为什么允许==返回除a之外的任何东西bool?如果返回int,则不能再将其用作条件语句,例如。if(a == b)将不再编译。重点是什么?
BlueRaja-Danny Pflughoeft

2
您可以返回一个对象,该对象具有向bool的隐式转换(运算符true / false),但是我仍然看不到在哪里有用。
Stefan Dragnev

53

可能是针对某人是否需要实现三值逻辑(即null)。在这种情况下-例如ANSI标准SQL-不能简单地根据输入否定运算符。

您可能遇到以下情况:

var a = SomeObject();

a == true返回falsea == false也返回false


1
在这种情况下,我想说的是,由于a不是布尔值,您应该将其与三态枚举进行比较,而不是实数truefalse
布莱恩·戈登

18
顺便说一句,我不能相信任何人的引用的FileNotFound笑话..
布莱恩·戈登·

28
如果a == true是的false话,我总是希望a != true成为true,尽管a == falsefalse
Fede

20
@Fede撞到了头。 这确实与问题无关 -您正在解释为什么有人可能要覆盖==,而不是为什么应该强迫他们覆盖两者。
BlueRaja-Danny Pflughoeft

6
@Yuck-是的,有一个很好的默认实现:这是常见的情况。即使在您的示例中,的默认实现也是!=正确的。在少数情况下,我们想要x == y并且x != y两者都为假(我想不出一个有意义的例子),它们仍然可以覆盖默认实现并提供自己的实现。 始终将常见情况设为默认!
BlueRaja-Danny Pflughoeft

25

除了C#在许多领域都遵循C ++之外,我能想到的最佳解释是,在某些情况下,您可能想要采用稍微不同的方法来证明“不平等”而不是证明“平等”。

显然,例如,通过字符串比较,您可以测试是否相等,并return在看到不匹配的字符时跳出循环。但是,它可能没有那么复杂的问题。我想到了绽放过滤器;快速判断元素是否不在集合中非常容易,但是很难判断元素是否在集合中。尽管return可以应用相同的技术,但是代码可能并不那么漂亮。


6
很好的例子;我认为您正在寻找原因。一个简单的!把您锁定在平等的单一定义,在知情的编码器可能能够优化 ==!=
Yuck

13
因此,这解释了为什么应该给他们选择覆盖两者的选择,而不是为什么要强迫他们选择。
BlueRaja-Danny Pflughoeft

1
因此,他们不必同时优化两者,而只需提供两者的明确定义
杰斯珀,

22

如果您在.net源代码中查看==和!=重载的实现,则它们通常不将!=实现为!(左==右)。他们使用否定逻辑完全实现了它(例如==)。例如,DateTime实现==作为

return d1.InternalTicks == d2.InternalTicks;

和!= as

return d1.InternalTicks != d2.InternalTicks;

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

return !(d1==d2);

那么您将对类引用中的==和!=的内部实现进行假设。避免这种假设可能是他们决定背后的哲学。


17

要回答您的编辑,关于为什么如果您覆盖两个都必须覆盖这两个原因,则全部在继承中。

如果覆盖==,则很可能会提供某种语义或结构上的相等性(例如,即使DateTimes的InternalTicks属性相等,即使它们可能是不同的实例,DateTimes也相等),然后您将操作符的默认行为从对象,它是所有.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。


14

如果对自定义类型重载==,而不对!=重载,则对象!= object的!=运算符将对其进行处理,因为一切都是从对象派生的,这与CustomType!= CustomType有很大不同。

同样,语言创建者可能希望通过这种方式为编码人员提供最大的灵活性,并且这样他们就不必对您打算做的事情做任何假设。



2
灵活性的原因也可以应用于他们决定放弃的许多功能,例如blogs.msdn.com/b/ericlippert/archive/2011/06/23/…。因此,这并不能说明他们选择的功能实施平等经营者。
Stefan Dragnev

那是非常有趣的。似乎这将是一个有用的功能。我不知道为什么他们会把那一个排除在外。
克里斯·穆林斯

11

这是我首先想到的:

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

5
在第一种情况下,您可以根据不平等测试来实施平等测试。
Stefan Dragnev

第二种情况将破坏像DictionaryList如果您对它们使用自定义类型的结构。
Stefan Dragnev

2
@Stefan:Dictionary使用GetHashCodeEquals,而不是运算符。List使用Equals
丹·阿布拉莫夫

6

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

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


1
在另一个方面实施是一种极好的实践。我刚刚看到它不这样做-容易出错。
Stefan Dragnev

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

@GolezTrol:您能解释一下Lua参考吗?我对Lua一点都不熟悉,但是您的引用似乎暗示了一些东西。
2011年

@Ziyao Wei:OP指出Lua的做法与C#不同(Lua显然确实为提到的运算符添加了默认副本)。只是试图平息这种比较。如果根本没有差异,那么至少其中之一没有权利存在。必须说我也不认识Lua。
GolezTrol

6

您问题中的关键词是“ 为什么 ”和“ 必须 ”。

结果是:

之所以这样回答,是因为他们设计的是这样,但这是正确的……但不能回答问题的“为什么”部分。

回答有时独立地覆盖这两个选项有时会有所帮助,这是正确的……但不能回答问题的“必须”部分。

我认为简单的答案是,没有任何令人信服的理由解释为什么C#要求您覆盖两者。

应该让你只覆盖语言==,并为您提供的默认实现!=就是!这一点。如果您碰巧也想覆盖!=,请尝试一下。

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


5

只是为了添加出色的答案在这里:

考虑当您尝试进入一个运算符并最终成为一个运算符时,在调试器中会发生什么!谈论混乱!!===

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


3

编程语言是异常复杂的逻辑语句的句法重排。考虑到这一点,您是否可以定义平等的情况而不定义不平等的情况?答案是不。为了使对象a与对象b相等,则对象a的逆不等于b也必须为真。另一种显示方式是

if a == b then !(a != b)

这为语言提供了确定对象相等性的确定能力。例如,比较NULL!= NULL可能会使未实现非等式语句的等式系统的定义受到影响。

现在,关于!=的想法只是像

if !(a==b) then a!=b

我不能同意这一点。但是,C#语言规范小组很可能决定迫使程序员一起明确定义对象的相等和不相等。


Null只会是一个问题,因为它null是的有效输入==,尽管显然很方便,但不应该这样。就我个人而言,我只是认为它允许进行大量模糊,草率的编程。
nicodemus13 2014年

2

简而言之,强制一致性。

无论您如何定义,“ ==”和“!=”始终是正确的对立面,它们通过“等号”和“不等号”的口头定义来定义。通过仅定义其中之一,您就可以面对等号运算符不一致的问题,对于两个给定值,其中'=='和'!='都可以为true或都为false。您必须同时定义两者,因为当您选择定义一个时,还必须适当地定义另一个,以使您清楚地知道“平等”的定义是什么。编译器的另一种解决方案是只允许您覆盖'=='或'!=',而使另一种固有地否定另一种。显然,C#编译器并非如此,我敢肯定

您应该问的问题是“为什么需要覆盖运算符?” 这是一个需要做出强有力推理的强有力决定。对于对象,“ ==”和“!=”通过引用进行比较。如果要覆盖它们以不通过引用进行比较,那么您将创建一个通用运算符不一致,这对于任何其他会仔细阅读该代码的开发人员来说都是不明显的。如果试图问“这两个实例的状态是否相等?”的问题,则应实现IEquatible,定义Equals()并利用该方法调用。

最后,IEquatable()出于相同的原因未定义NotEquals():可能导致相等运算符不一致。NotEquals()应该总是返回!Equals()。通过将NotEquals()的定义开放给实现Equals()的类,您再次在确定相等性时强加了一致性问题。

编辑:这只是我的推理。


这是不合逻辑的。如果原因是一致性,则适用相反的情况:您将只能实现operator ==,而编译器将推断operator !=。毕竟,这就是operator +and的作用operator +=
康拉德·鲁道夫

1
foo + = bar; 是复合赋值,是foo = foo + bar;的简写。它不是运算符。
blenderfreaky

如果他们确实 “总是正确的对立”,那么这将是一个理由自动定义a != b!(a == b)...(这是不是C#一样)。
andrewf '19

-3

也许只是他们没有想到的事情没有时间做。

当我重载==时,我总是使用您的方法。然后,我只在另一个中使用它。

您是对的,只需少量工作,编译器便可以免费将其提供给我们。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.