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

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

为什么?

我很想知道为什么devise者认为这是必要的,为什么当编译器只有对方存在的时候,编译器为什么不能默认合理的实现呢? 例如,Lua只允许你定义相等运算符,而你可以免费获得另一个。 C#可以通过要求你定义==或者==和!=然后自动编译缺less的!=运算符为!(left == right)

我知道有些奇怪的angular落案例,其中一些实体可能既不平等也不平等(如IEEE-754 NaN's),但是那些看起来是例外而非规则。 所以这并不能解释为什么C#编译器的devise者将这个规则作为例外。

我已经看到平等运算符定义不好的情况下,那么不平等运算符就是一个复制粘贴,每一个比较都是相反的,每一个&&切换到一个||。 (你明白了……基本上(a == b)通过De Morgan规则扩展了)。 这是一个糟糕的做法,编译器可以通过devise来消除,就像Lua一样。

注意:对于运算符<> <=> =也是一样。 我无法想象你需要以非自然的方式来定义这些情况。 Lua让你自然地定义<和<=,并定义> =和>自然地通过前人的否定。 为什么C#不这样做(至less默认是')?

编辑

显然有一些正当理由可以让程序员对他们喜欢的平等和不平等进行检查。 有些答案指出,这可能是很好的情况。

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

Object.EqualsIEquatable.Equals IEqualityComparer.Equals等deviseselect形成鲜明对比的是,缺lessNotEquals对象表明框架认为!Equals()对象是不平等的,就是这样。 而且像Dictionary这样的类和像.Contains()这样的方法完全依赖于前面提到的接口,即使定义了,也不直接使用操作符。 实际上,当ReSharper生成相等成员时,它根据Equals()定义了==!= ,并且只有在用户select生成运算符的时候。 理解对象平等的框架不需要相等运算符。

基本上,.NET框架并不关心这些操作符,它只关心几个Equals方法。 要求用户同时定义==和!=运算符的决定完全与.NET的语言devise和对象语义有关。

我不能为语言devise师说话,但是从我能理解的angular度来看,这似乎是故意的,适当的devise决定。

看看这个基本的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 

这看起来确实如此。 它仅在==上创build一个相等比较器,并检查该类的内部值是否相等。

虽然您不能在C#中创build这样的类,但您可以使用为.NET编译的类。 很明显,它将使用我们的重载运算符==所以,运行时使用什么!=

C#EMCA标准有一大堆规则(第14.9节),解释了在评估相等性时如何确定使用哪个运算符。 为了使其过于简化,因此不是完全准确的,如果被比较的types是相同的types, 并且存在重载的等同运算符,则将使用该重载而不是从Objectinheritance的标准引用等同运算符。 那么,如果只有一个操作符存在,它将使用默认的引用相等运算符,即所有对象都有,没有超载。 1

了解到这一点,真正的问题是:为什么这样devise,为什么编译器不能自行解决呢? 很多人都说这不是一个devise的决定,但我喜欢认为这是这样想的,尤其是所有的对象都有一个默认的相等运算符。

那么,为什么编译器不能自动创build!=运算符? 除非有人从微软确认这一点,否则我无法确定,但这是我可以从事实推断中确定的。


为了防止意外的行为

也许我想对==做一个值比较来testing相等性。 然而,当它来到!=我根本不关心,如果价值是平等的,除非参考是平等的,因为我的程序认为他们是平等的,我只关心,如果引用匹配。 毕竟,这实际上被概括为C#的默认行为(如果两个运算符都没有被重载,就像在用另一种语言编写的一些.net库的情况下那样)。 如果编译器自动添加代码,我不能再依靠编译器来输出符合要求的代码。 编译器不应该编写隐藏的代码来改变你的行为,特别是当你编写的代码在C#和CLI的标准之内。

强迫你超载的问题上,我只能坚持认为这是标准(EMCA-334 17.9.2) 2 ,而不是默认行为。 该标准没有说明为什么。 我相信这是由于C#借用C ++的许多行为。 有关详情,请参阅下文。


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

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

 public static int operator ==(MyClass a, MyClass b) { return 0; } 

和这个一样有效:

 public static bool operator ==(MyClass a, MyClass b) { return true; } 

如果你要返回的不是bool,编译器不能自动推断出相反的types。 而且,在你的运算符返回bool的情况下,创build只存在于特定情况下的生成代码或者如上所述隐藏CLR默认行为的代码是没有意义的。


C#借用C ++ 3很多

当C#引入时,MSDN杂志上有一篇文章写道,谈论C#:

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

是的,C#的devise目标是提供与C ++几乎相同的权力,为了方便起见,像刚性types安全和垃圾收集,牺牲一点点。 C#在C ++之后强烈build模。

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

现在,C ++并不直接要求你重载互补操作符。 如果你在示例程序中编译了代码,你会看到它没有错误地运行。 但是,如果您尝试添加该行:

 cout << (a != b); 

你会得到

编译器错误C2678(MSVC):binary'!=':没有find操作符types为'Test'(或没有可接受的转换)的左操作数。

所以,虽然C ++本身并不要求你成对地重载,但它不会让你使用一个你没有在自定义类上重载的相等运算符。 它在.NET中是有效的,因为所有的对象都有一个默认的对象; C ++没有。


1.顺便提一下,如果你想重载任何一个操作符,C#标准仍然要求你重载这对操作符。 这是标准的一部分,而不是简单的编译器 但是,在访问使用另一种不具有相同要求的语言编写的.net库时,与确定要调用哪个操作符相同的规则也适用。

2. EMCA-334(pdf) ( publications/files/ECMA-ST/Ecma-334.html

和Java,但这真的不重要

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

您可能会遇到以下情况:

 var a = SomeObject(); 

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

除了C#在许多方面都遵循C ++之外,我能想到的最好的解释是,在某些情况下,您可能想采取一种略微不同的方法来certificate“不平等”,而不是certificate“平等”。

显然,在string比较中,例如,当你看到不匹配的字符时,你可以testing它们是否相等并return循环。 但是,这个问题可能并不那么干净,还有更复杂的问题。 想起布隆filter ; 快速判断元素是否在集合中是很容易的,但很难判断元素是否在集合中。 虽然可以使用相同的return技术,但是代码可能不那么漂亮。

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

 return d1.InternalTicks == d2.InternalTicks; 

和!= as

 return d1.InternalTicks != d2.InternalTicks; 

如果你(或编译器,如果它隐式地)实现!= as

 return !(d1==d2); 

那么你就在你的类所引用的东西中对==和!=的内部实现做了一个假设。 避免这种假设可能是他们决定背后的哲学。

要回答你的编辑,关于你为什么被强制覆盖,如果你重写,这一切都在inheritance。

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

在几乎任何其他的方法压倒一切的情况下,假定重写一种方法也会导致行为改变为反义方法是不合逻辑的。 如果您使用Increment()和Decrement()方法创build了一个类,并且在子类中覆盖了Increment(),那么您是否期望Decrement()也被覆盖的行为的相反部分覆盖? 在所有可能的情况下,编译器都不能够足够聪明地为操作符的任何实现生成反函数。

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

(a <b)== a> = b和!(a>>)应该问的REAL问题是为什么<和>和<=和> =是比较运算符的对, b)== a <= b。 你应该被要求实现所有四个,如果你重写一个,你可能应该要求重写==(和!=),因为(a <= b)==(a == b)如果a是语义的等于b。

如果你为自定义types重载==而不是!=那么它将由!=操作符处理对象!=对象,因为所有东西都是从对象派生的,并且这将比CustomType!= CustomType大不相同。

另外语言创build者可能希望这样做,以便为编程人员提供最大的灵活性,也使他们不会对你打算做什么做出假设。

这是我首先想到的:

  • 如果testing不平等要比testing平等快得多呢?
  • 如果在某些情况下, 你想要为==!=返回false (即,如果由于某种原因不能比较它们)

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

结果是:

回答是这样的,因为他们devise的是这样,是真实的…但不回答你的问题的“为什么”的一部分。

回答这有时可能会有帮助,这两个独立的重写是真实的…但不回答你的问题的“必须”的一部分。

我认为简单的答案是没有任何令人信服的理由,为什么C# 要求你重写两个。

该语言应该允许您只覆盖== ,并为您提供!=的默认实现! 那。 如果你碰巧想重写!= ,那就!=

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

那么,这可能只是一个deviseselect,但正如你所说, x!= y不必与!(x == y) 。 通过不添加默认实现,您可以确定不能忘记实现特定的实现。 如果它的确如你所说的那么微不足道,那么你可以用另一个来实现一个。 我不明白这是如何“不好的做法”。

C#和Lua之间可能还有一些其他的区别

只要在这里添加优秀的答案:

考虑一下debugging器会发生什么情况,当你尝试进入一个!=运算符,而最终在一个==运算符! 谈论混乱!

CLR将允许你自由地排除一个或另一个操作员 – 因为它必须使用多种语言。 但是有很多C#的例子没有公开CLR特性(例如ref和locals),还有大量的实现不在CLR中的特性的例子(例如: usinglockforeach等)。

编程语言是非常复杂的逻辑语句的语法重新排列。 考虑到这一点,你可以定义一个平等的案例,而不是定义一个不平等的情况? 答案是不。 对于一个对象a等于对象b,那么对象a的倒数不等于b也必须为真。 另一种显示方式是

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

这为语言确定对象的平等性提供了确定的能力。 例如,比较NULL!= NULL可以将一个扳手放入一个没有实现不等于语句的等式系统的定义中。

现在,关于!=简单地被定义为可replace的概念

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

我不能争辩。 然而,这很可能是C#语言规范组决定的,程序员被迫明确定义一个对象的相等和不相等

总之,强制一致性。

'=='和'!='永远是真正的对立面,无论你如何定义它们,都是通过“等于”和“不等于”的口头定义来定义的。 通过只定义其中的一个,你可以打开一个等于运算符的不一致性,其中'=='和'!='都可以为真,或者对于两个给定的值都是假的。 你必须定义两者,因为当你select定义一个时,你还必须适当地定义另一个,这样就可以清楚地知道你对“相等”的定义是什么。 编译器的另一个解决scheme是只允许你重写'=='或者'!=',而另一个解决scheme是固有地否定另一个。 很明显,C#编译器并不是这样,我确信有一个合理的原因可以归结为简单的select。

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

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

编辑:这只是我的推理。

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

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

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