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

c#编译器要求每当自定义类型定义操作符==时,它也必须定义!=(参见在这里)。

为什么?

我很好奇为什么设计人员认为这是必要的,为什么编译器不能默认为一个合理的实现操作符时,只有另一个存在。例如,Lua只允许定义相等操作符,而免费获得另一个。c#也可以做到这一点,要求你定义==或同时定义==和!=,然后自动将缺少的!=运算符编译为!(left == right)

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

我见过一些糟糕的情况,定义了相等运算符,然后不等式运算符是一个复制粘贴,每个比较都是反向的,每个&&换成||(你懂的…(a==b)通过德摩根法则展开)。这是编译器可以通过设计消除的不良实践,就像Lua一样。

< p >注意: 这同样适用于操作员<> <= >=。我无法想象在哪些情况下需要用非自然的方式来定义它们。Lua允许你只定义<和<=,通过前两者的否定自然地定义了>=和>。为什么c#不做同样的事情(至少在默认情况下)?< / p >

编辑

显然,有充分的理由允许程序员根据自己的喜好执行相等和不相等的检查。一些答案指向了这样做可能不错的案例。

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

它也与。net接口的设计选择形成鲜明对比,如Object.EqualsIEquatable.Equals IEqualityComparer.Equals,其中缺少NotEquals对应对象表明框架认为!Equals()对象是不平等的,就是这样。此外,像Dictionary这样的类和像.Contains()这样的方法只依赖于前面提到的接口,即使定义了操作符,也不直接使用操作符。事实上,当ReSharper生成相等成员时,它根据Equals()定义了==!=,即使这样,也只是在用户选择生成操作符时才这样做。框架不需要相等操作符来理解对象相等。

基本上,. net框架并不关心这些操作符,它只关心一些Equals方法。要求用户同时定义==和!=操作符的决定纯粹与语言设计有关,而与. net关心的对象语义无关。

14411 次浏览

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

我总是用你的方法当我超载==。然后我把它用在另一个。

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

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

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

可能用于如果有人需要实现三值逻辑(即null)。在这种情况下——例如,ANSI标准SQL——操作符不能简单地根据输入进行求反。

你可以有这样的情况:

var a = SomeObject();

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

除了c#在许多方面遵从c++之外,我能想到的最好的解释是,在某些情况下,你可能想要采用不同的方法来证明“不相等”而不是证明“相等”。

显然,以字符串比较为例,当你看到不匹配的字符时,你可以在循环外测试相等性和return。然而,对于更复杂的问题,它可能就不那么清晰了。布隆过滤器会出现在脑海中;在集合中快速判断元素是否为是非常容易的,但是在集合中判断元素是否为是非常困难的。虽然同样的return技术可以应用,但代码可能不那么漂亮。

如果你看一下。net源代码中==和!=重载的实现,它们通常不会实现!= as !(左==右)。他们用否定的逻辑完全实现它(比如==)。例如,DateTime实现了== as

return d1.InternalTicks == d2.InternalTicks;

And != as

return d1.InternalTicks != d2.InternalTicks;

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

return !(d1==d2);

然后,您要对类引用的对象中==和!=的内部实现做一个假设。避免这种假设可能是他们决定背后的哲学。

编程语言是对异常复杂的逻辑语句的语法重排。考虑到这一点,你能定义一个相等的情况而不定义一个不相等的情况吗?答案是否定的。如果一个物体a等于物体b,那么物体a的逆不等于b也必须成立。另一种表达方式是

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

这为语言确定对象的相等性提供了明确的能力。例如,比较NULL != NULL可能会对没有实现非相等语句的相等系统的定义产生不利影响。

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

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

我不能反驳这一点。然而,这很可能是c#语言规范组的决定,程序员被迫显式地定义对象的相等性和非相等性

如果你为你的自定义类型重载==,而不是!=,那么它将由object != object的!=操作符处理,因为所有东西都是从object派生的,这将与CustomType != CustomType有很大不同。

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

这是我首先想到的:

  • 如果测试不平等比测试平等快得多?
  • 如果在某些情况下你需要为__ABC1和__ABC2返回false(即如果由于某种原因它们不能进行比较)

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

结果是:

这样回答是因为他们设计了这样,这是真的……但不回答你的"为什么"

回答说,有时独立地覆盖这两个可能是有帮助的,是真的…但没有回答你"必须"的问题。

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

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

这不是一个好的决定。人类设计语言,人类并不完美,c#也不完美。耸耸肩,回答问题

我不能代表语言设计师说话,但从我的推理来看,这似乎是有意为之,合理的设计决策。

看看这个基本的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继承的标准引用相等操作符。因此,如果只有一个操作符存在,它将使用所有对象都具有的默认引用相等操作符,它没有重载。__abc1也就不足为奇了

知道了这种情况后,真正的问题是:为什么要这样设计,为什么编译器不自己解决这个问题?很多人都说这不是一个设计决策,但我喜欢这样认为,特别是考虑到所有对象都有一个默认的相等运算符。

那么,为什么编译器不自动创建!=操作符呢?我不能确定,除非微软的人证实这一点,但这是我可以从事实推理得出的结论。


防止意外行为

也许我想在==上做一个值比较来测试是否相等。然而,当涉及到!=时,我根本不关心值是否相等,除非引用相等,因为对于我的程序来说,它们相等,我只关心引用是否匹配。毕竟,这实际上是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所示

现在,c++不直接需要你重载补充操作符。如果编译示例程序中的代码,您将看到它运行时没有错误。然而,如果你尝试添加一行:

cout << (a != b);

你会得到

编译器错误C2678 (MSVC):二进制'!=':没有找到左操作数为'Test'类型的操作符(或者没有可接受的转换)'。

因此,虽然c++本身不要求你成对重载,但它不会允许你使用一个在自定义类上没有重载过的相等操作符。它在。net中是有效的,因为所有对象都有一个默认的对象;c++则不然。


1. 顺便说一句,如果您想重载其中一个操作符,c#标准仍然要求您重载这对操作符。这是标准的一部分,而不仅仅是编译器。然而,当您访问用另一种语言编写的.net库时,关于调用哪个操作符的确定规则也适用,而这种语言没有相同的要求。

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

3.还有Java,但这不是重点

要回答您的编辑,关于为什么如果您覆盖了一个,就必须覆盖两个,这都在继承中。

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

在几乎所有其他方法重写的情况下,假定重写一个方法也会导致反义词方法的行为变化是不合逻辑的。如果你用Increment()和Decrement()方法创建了一个类,并在子类中重写了Increment(),那么你是否期望Decrement()也会被重写,与你被重写的行为相反?编译器不能聪明到在所有可能的情况下为运算符的任何实现生成逆函数。

然而,尽管操作符的实现与方法非常相似,但在概念上是成对工作的;== and !=, <> <= >=。在这种情况下,从消费者的角度来看,认为!=与==的工作方式有任何不同是不合逻辑的。因此,编译器不能假定a!=b == !(a==b)在所有情况下,但通常期望==和!=应该以类似的方式操作,因此编译器强制您成对实现,但实际上您最终这样做了。如果,对于你的班级,a!=b == !(a==b),然后使用!(==)简单地实现!=操作符,但如果该规则在您的对象的所有情况下都不成立(例如,如果与特定值的比较,相等或不相等,是无效的),那么您必须比IDE更聪明。

真正应该问的问题是为什么?>和<=和>=是比较运算符对,当以数字形式出现时,它们必须同时实现!b) == a >= b和!(a > b) == a <= b。如果你覆盖其中一个,你应该被要求实现所有四个,你可能也应该被要求覆盖==(和!=),因为(a <= b) == (a == b)如果a在语义上等于b。

在这里补充一下精彩的答案:

考虑一下在调试器中会发生什么,当你试图进入!=操作符,而最终却进入了==操作符!真让人困惑!

CLR允许您自由地省略一个或另一个操作符,这是有道理的——因为它必须与许多语言一起工作。但是有很多c#不公开CLR特性的例子(例如ref返回和局部变量),也有很多实现CLR本身之外的特性的例子(例如:usinglockforeach等)。

简而言之,强迫一致性。

'=='和'!='总是真正的反义词,无论你如何定义它们,它们的定义是由“等于”和“不等于”的口头定义定义的。如果只定义其中一个,就会导致'=='和'!='对于两个给定的值可以都为真,也可以都为假。你必须定义两者,因为当你选择定义一个时,你也必须适当地定义另一个,这样你对“平等”的定义是明确的。编译器的另一个解决方案是只允许你重写'==' OR '!=',并让另一个在本质上否定另一个。显然,c#编译器不是这样的,我相信有一个合理的理由,可以严格归因于简单性的选择。

你应该问的问题是“为什么我需要重写操作符?”这是一个强有力的决定,需要强有力的推理。对于对象,'=='和'!='通过引用进行比较。如果您要将它们重写为NOT compare by reference,则您正在创建一个通用的运算符不一致,这对于任何其他阅读该代码的开发人员来说都是不明显的。如果你想问“这两个实例的状态是等价的吗?”,那么你应该实现IEquatible,定义Equals()并利用该方法调用。

最后,由于同样的原因,IEquatable()没有定义NotEquals():可能会导致相等运算符不一致。NotEquals()应该总是返回!通过将NotEquals()的定义开放给实现Equals()的类,您再次迫使确定相等性的一致性问题。

编辑:这只是我的理由。