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

为什么?

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

我知道有些奇怪的情况实体可能既不相等也不不相等(例如IEEE-754 NaN),但它们看起来像是例外,而不是规则。因此,这并不能解释为什么C#编译器设计人员将例外设置为规则。

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

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

EDIT

显然,有充分的理由允许程序员对相等和不相等进行检查。但是他们喜欢。

但是,我的问题的核心是,为什么C#在通常在逻辑上不必要时会强制使用C#?

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

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

评论

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

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

我确定@Eric Lippert也可以提供合理的答案。

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

对于Python的相同问题:stackoverflow.com/questions/4969629/…

#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的情况下,对于他们而言,创建仅在该特定情况下才存在的生成代码,或者如上所述隐藏了CLR缺省行为的代码,这对他们没有意义。


C#大量借鉴了C ++ 3


引入C#时,MSDN杂志上有一篇文章提到了C# :


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


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

您可能不会惊讶于在C ++中,相等运算符不必返回布尔值,如本示例程序所示

现在,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,但这实际上不是重点。

评论


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

–布赖恩·戈登(Brian Gordon)
2011年8月2日在22:08



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

– Dan Diplo
2011年8月3日,晚上8:21

@丹·迪普洛(Dan Diplo),没关系,这不是不禁止我不这样做或不这样做。

–克里斯托弗·柯伦斯(Christopher Currens)
2011年8月3日15:51

如果为true,则我认为#2是正确的;但是现在的问题变成了,为什么允许==返回除bool之外的任何内容?如果返回整数,则不能再将其用作条件语句,例如。 if(a == b)将不再编译。重点是什么?

– BlueRaja-Danny Pflughoeft
2011年8月3日在16:54

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

– Stefan Dragnev
2011年8月3日19:49

#2 楼

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

您可能会遇到以下情况:

var a = SomeObject();


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

评论


在那种情况下,我会说,因为a不是布尔值,所以您应该将其与三态枚举进行比较,而不是真或假。

–布赖恩·戈登(Brian Gordon)
2011年8月2日在18:50

顺便说一句,我不敢相信没有人引用FileNotFound的笑话。

–布赖恩·戈登(Brian Gordon)
2011年8月2日19:26



如果a == true为false,那么我总是希望!= true为true,尽管== false为false

–费德
2011年8月2日在20:11

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

– BlueRaja-Danny Pflughoeft
2011年8月2日在21:10



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

– BlueRaja-Danny Pflughoeft
2011年8月2日在22:14



#3 楼

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

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

评论


很好的例子;我认为您正在解释原因。一个简单的 !将您锁定在一个单一的相等性定义中,在此情况下,有见识的编码人员也许可以同时优化==​​和!=。

– Y
11年8月2日在19:05

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

– BlueRaja-Danny Pflughoeft
2011年8月2日在21:08



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

–杰斯珀
2011年8月4日在10:46

#4 楼

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

return d1.InternalTicks == d2.InternalTicks;


,而!= as

return d1.InternalTicks != d2.InternalTicks;


编译器(如果隐式执行的话)要实现!= as

return !(d1==d2);


然后您就可以假设==和!=的内部实现您的课程正在引用。避免这种假设可能是他们做出决定的基本理念。

#5 楼

要回答您的编辑,关于为什么要重写两个都被强制重写的原因,所有这些都在继承中。

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

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

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

应该问的REAL问题是,为什么在用数字表示!(a = b和!(a>时,为什么<和>和<=和> =是必须同时实现的比较运算符对? b)== a <= b。如果要覆盖全部四个,则应要求全部实现这四个,并且可能还应要求覆盖==(和!=),因为如果(a <= b)==(a == b)如果a在语义上等于b。

#6 楼

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

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

评论


您启发了这个问题:stackoverflow.com/questions/6919259/…

–布赖恩·戈登(Brian Gordon)
2011年8月2日在22:09

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

– Stefan Dragnev
2011年8月3日在7:04



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

–克里斯·穆林斯(Chris Mullins)
2011年8月3日,10:50

#7 楼

这是我首先想到的:


如果测试不平等比测试平等快得多怎么办?
如果在某些情况下您希望同时返回false==!=(即,由于某些原因而无法比较它们)怎么办


评论


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

– Stefan Dragnev
2011年8月2日在20:06

如果您对它们使用自定义类型,则第二种情况将破坏字典和列表之类的结构。

– Stefan Dragnev
2011年8月2日在20:07

@Stefan:Dictionary使用GetHashCode和Equals,而不是运算符。列表使用等于。

–丹·阿布拉莫夫(Dan Abramov)
2011年8月2日在20:09

#8 楼

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

C#和Lua之间也可能存在其他差异...

评论


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

– Stefan Dragnev
2011年8月2日,18:45



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

– GolezTrol
2011年8月2日在18:48

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

– zw324
2011年8月2日在18:50

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

– GolezTrol
2011年8月2日在18:53



#9 楼

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

结果:

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

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

我认为简单的答案是C#要求您同时覆盖这两者没有任何令人信服的理由。

该语言应只允许您覆盖== ,并为您提供!=的默认实现,即!。如果您碰巧也想覆盖!=,请尝试一下。

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

#10 楼

只需在此处添加出色的答案即可:

当您尝试进入!=运算符并最终使用==运算符时,请考虑一下调试器中会发生什么!谈论混淆吧!

有道理的是,CLR允许您自由地省略一个或另一个运算符-因为它必须支持多种语言。但是,有很多C#的示例未公开CLR功能(例如,ref返回值和本地变量),还有许多示例未在CLR本身中实现功能(例如:usinglockforeach等)。

评论


在我看来,这实际上就像自动生成的运算符会产生的最坏的影响。不过,不要以为这是他们的理由。还有其他一些您未显式调用代码的示例,例如,您不是在本地使用的类的静态成员初始化,而是在不考虑它的情况下加载的。在了解了原因之后,人们很容易习惯了这一点。我不认为人们会为为什么在不定义而检查!=时为什么调用==感到困惑。我想他们最迟在阅读有关文档后会理解。

– LWChris
20年8月21日在16:24

#11 楼

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

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

现在,关于!=的想法只是可替换的定义,如

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

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

评论


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

–nicodemus13
2014年5月2日21:57

#12 楼

简而言之,强制性一致性。

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

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

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

编辑:这只是我的推理。

评论


这是不合逻辑的。如果原因是一致性,则适用相反的情况:您将只能实现==运算符,而编译器将推断!=。毕竟,这就是对运算符+和运算符+ =的作用。

–康拉德·鲁道夫(Konrad Rudolph)
11年5月5日在15:14

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

–blenderfreaky
19-10-24在6:57

如果确实是“总是对立的东西”,那么这就是将!= b自动定义为!(a == b)…(这不是C#所做的事情)的原因。

– andrewf
19/12/17在15:18

#13 楼

可能只是他们没有想到的事情而没有时间去做。

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

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