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

时间:2011-08-02 18:37:14

标签: c# language-design

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

为什么?

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

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

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

注意: 这同样适用于运营商< > < => =。我无法想象你需要以不自然的方式定义它们的情况。 Lua让你只定义<和< =并定义> =和>自然地通过前者的否定。为什么C#不做同样的事情(至少'默认')?

修改

显然有充分的理由允许程序员实现对他们喜欢的平等和不平等的检查。一些答案指出了可能很好的案例。

然而,我的问题的核心是为什么在

Object.EqualsIEquatable.Equals IEqualityComparer.Equals之类的.NET接口的设计选择形成鲜明对比,缺少NotEquals对应项表示框架认为{ {1}}对象不平等就是那样。此外,像!Equals()这样的类和像Dictionary这样的方法完全依赖于前面提到的接口,即使它们被定义也不直接使用运算符。实际上,当ReSharper生成平等成员时,它会根据.Contains()来定义==!=,即使这样,用户也会选择生成运算符。框架不需要等于运算符来理解对象的相等性。

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

13 个答案:

答案 0 :(得分:154)

答案 1 :(得分:47)

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

你可能会遇到以下情况:

var a = SomeObject();

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

答案 2 :(得分:24)

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

显然,对于字符串比较,例如,当您看到不匹配的字符时,您可以只测试相等性和return。但是,它可能不是那么干净,有更复杂的问题。我会想到bloom filter;很容易快速判断该元素中的元素不是,但很难判断该元素中的元素是否为。虽然可以应用相同的return技术,但代码可能不那么漂亮。

答案 3 :(得分:20)

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

return d1.InternalTicks == d2.InternalTicks;

和!= as

return d1.InternalTicks != d2.InternalTicks;

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

return !(d1==d2);

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

答案 4 :(得分: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。

答案 5 :(得分:12)

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

此外,语言创建者可能希望以这种方式为编码员提供最大的灵活性,同时也不会对你打算做什么做出假设。

答案 6 :(得分:9)

这是我首先想到的:

  • 如果测试不平等比测试平等要快得多怎么办?
  • 如果在某些情况下您要为false== 返回!=该怎么办(例如,如果因某些原因无法进行比较)

答案 7 :(得分:5)

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

结果:

回答是这样的,因为他们设计的是这样,是真的......但没有回答你问题的“为什么”部分。

回答说,有时候有必要单独覆盖这两者,这是真的......但不回答你问题的“必须”部分。

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

该语言应该只允许您覆盖==,并为您提供!=的默认实施!。如果你碰巧想要覆盖!=,也可以使用它。

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

答案 8 :(得分:4)

嗯,这可能只是一个设计选择,但正如您所说,x!= y不一定与!(x == y)相同。通过不添加默认实现,您可以确定不会忘记实现特定实现。如果它确实像你说的那样微不足道,你可以用另一个实现一个。我不明白这是“糟糕的做法”。

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

答案 9 :(得分:3)

在这里添加优秀的答案:

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

CLR允许你自由地遗漏一个或另一个操作符是有道理的 - 因为它必须与许多语言一起使用。但是有很多C#没有暴露CLR特性的例子(例如ref返回和本地),以及大量实现CLR本身不具备的特性的例子(例如:using,{{1 },lock等。)

答案 10 :(得分:2)

编程语言是异常复杂的逻辑语句的语法重排。考虑到这一点,你能定义一个平等的案例,而不是定义一个不平等的案例吗?答案是不。对于对象a等于对象b,则对象a的倒数不等于b也必须为真。显示此信息的另一种方法是

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

这为语言提供了确定对象相等性的明确能力。例如,比较NULL!= NULL可以将扳手放入不实现非等同语句的相等系统的定义中。

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

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

我无法与之争辩。但是,很可能是C#语言规范组决定程序员被迫明确定义对象的相等和不相等

答案 11 :(得分:2)

简而言之,强制一致。

无论你如何定义它们,

'=='和'!='都是真正的对立面,通过它们对“等于”和“不等于”的口头定义来定义。通过仅定义其中一个,您可以打开一个等于运算符不一致的地方,其中'=='和'!='都可以为true,或者两个给定值都为false。您必须定义两者,因为当您选择定义一个时,您还必须适当地定义另一个,以便明确清楚您对“相等”的定义是什么。编译器的另一个解决方案是只允许你覆盖'=='OR'!='并让另一个本身否定另一个。显然,C#编译器的情况并非如此,我确信有一个合理的理由可以归结为严格的选择。

你应该问的问题是“为什么我需要覆盖操作符?”这是一个强烈的决定,需要强有力的推理。对于对象,'=='和'!='按引用进行比较。如果要覆盖它们而不是通过引用进行比较,则会产生一般操作符不一致性,这对任何其他熟悉该代码的开发人员来说都是不明显的。如果您试图询问“这两个实例的状态是否等效?”,那么您应该实现IEquatible,定义Equals()并利用该方法调用。

最后,IEquatable()没有为相同的推理定义NotEquals():可能会打开相等运算符的不一致性。 NotEquals()应该总是返回!Equals()。通过向实现Equals()的类开放NotEquals()的定义,您再次强制确定相等性的一致性问题。

编辑:这只是我的推理。

答案 12 :(得分:-2)

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

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

你是对的,只需少量工作,编译器就可以免费提供给我们。

相关问题