关于语言设计:为什么我们必须在C#中定义==和!=?

Why must we define both == and != in C#?

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

为什么?

我很好奇为什么设计者认为这是必要的,为什么编译器不能在只有另一个操作符存在的情况下,为两个操作符之一默认一个合理的实现。例如,Lua只允许您定义相等运算符,而另一个运算符是免费的。C也可以这样做,要求您定义==或同时定义==和!=然后自动编译丢失的!=作为!(left == right)的运算符。

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

我曾经看到过一些粗制滥造的情况,其中定义了相等运算符,然后不相等运算符是一个复制粘贴,每个比较都颠倒,每个&;&;都切换到一个(您得到的点是……基本上!(a==b)通过德摩根的规则扩展)。编译器可以通过设计消除这种糟糕的实践,就像Lua一样。

注:对于运算符<><=>=,同样适用。我无法想象你需要用非自然的方式来定义这些。Lua只允许您定义<和<=并通过Formers的否定自然定义>=和>。为什么C不做同样的事情(至少是"默认的")?

编辑

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

然而,我的问题的核心是,为什么在通常逻辑上不必要的情况下,在C中强制要求这样做?

与.NET接口的设计选择(如Object.EqualsIEquatable.EqualsIEqualityComparer.Equals形成鲜明对比,其中缺少NotEquals对应项表明框架认为!Equals()对象是不平等的,即。此外,像Dictionary这样的类和像.Contains()这样的方法只依赖于上述接口,即使定义了这些接口,也不直接使用操作符。事实上,当Resharper生成相等成员时,它用Equals()定义==!=,甚至只有当用户选择生成运算符时。框架不需要相等运算符来理解对象相等。

基本上,.NET框架不关心这些操作符,它只关心一些Equals方法。决定同时要求==和!=要由用户串联定义的运算符仅与语言设计相关,而与.NET无关。


我不能代表语言设计者说话,但从我的推理来看,这似乎是有意的,正确的设计决策。好的。

查看这个基本的F代码,您可以将其编译成一个工作库。这是f的合法代码,仅重载相等运算符,而不是不相等:好的。

1
2
3
4
5
6
7
8
9
10
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节),解释了在评估相等性时如何确定要使用的运算符。为了使其过于简化,因而不完全准确,如果要比较的类型属于同一类型,并且存在重载的相等运算符,则它将使用该重载,而不是继承自对象的标准引用相等运算符。因此,如果只存在一个运算符,它将使用所有对象都具有的默认引用相等运算符,这并不奇怪。1好的。

既然知道了这一点,真正的问题是:为什么这样设计,为什么编译器不自己解决呢?很多人都说这不是一个设计决策,但我喜欢这样想,特别是考虑到所有对象都有一个默认的相等操作符。好的。

那么,为什么编译器不自动创建!=操作符呢?除非有微软的人证实,否则我无法确定,但这是我根据事实推理得出的结论。好的。防止意外行为

也许我想在==上做一个值比较来测试相等性。然而,当涉及到!=时,我根本不关心值是否相等,除非引用相等,因为我的程序认为它们相等,我只关心引用是否匹配。毕竟,这实际上被概括为C的默认行为(如果两个操作符都没有过载,就像用另一种语言编写的某些.NET库那样)。如果编译器自动添加代码,我就不能再依赖编译器来输出应该兼容的代码了。编译器不应该编写更改您行为的隐藏代码,尤其是当您编写的代码同时符合C和CLI的标准时。好的。

关于它强制您超载它,而不是去默认行为,我只能坚定地说它在标准(EMCA-334 17.9.2)2中。本标准未规定原因。我相信这是因为C语言借用了C++的很多行为。有关更多信息,请参见下文。好的。当您覆盖!===时,您不必返回bool。

这是另一个可能的原因。在c中,此函数:好的。

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

与此相同有效:好的。

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

如果返回的不是bool,编译器将无法自动推断出相反的类型。此外,在您的运算符返回bool的情况下,对它们来说,创建只存在于该特定情况下的生成代码,或者如我前面所说,隐藏clr默认行为的代码,是没有意义的。好的。C++从C++ 3中大量借用

当C被介绍时,有一篇文章在《msdn》杂志上写道,谈论C:好的。

Many developers wish there was a language that was easy to write, read, and maintain like Visual Basic, but that still provided the power and flexibility of C++.

Ok.

是的,C的设计目标是提供几乎相同的电量,如C++,只牺牲了一些方便,如刚性类型安全和垃圾收集。C +是强模型后,C++。好的。

在C++中,平等操作符不必返回BOOL,您可能不会感到惊讶,如本示例程序所示好的。

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

1
cout << (a != b);

你会得到好的。

compiler error C2678 (MSVC) : binary '!=' : no operator found which takes a left-hand operand of type 'Test' (or there is no acceptable conversion)`.

Ok.

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

1。另一方面,C标准仍然要求您在想要重载其中一个运算符时重载这对运算符。这是标准的一部分,而不仅仅是编译器。但是,当您访问用另一种语言编写的不具有相同要求的.NET库时,有关确定要调用哪个运算符的规则同样适用。好的。

2。EMCA-334(pdf)(http://www.ecma-international.org/publications/files/ecma-st/ecma-334.pdf)好的。

三。和Java,但这不是重点好的。好啊。


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

你可能会遇到这样的情况:

1
var a = SomeObject();

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


除了C在许多领域中对C++的延迟之外,我能想到的最好的解释是,在某些情况下,你可能想用一种稍微不同的方法来证明"不平等",而不是证明"平等"。

显然,通过字符串比较,例如,当您看到不匹配的字符时,您可以在循环外测试equality和return。但是,它可能不太干净,有更复杂的问题。布卢姆滤波器出现在人们的脑海中;很容易快速判断元件是否在集合中,但很难判断元件是否在集合中。虽然可以应用相同的return技术,但代码可能不那么漂亮。


如果您查看的是==和!的重载的实现。=在.NET源代码中,它们通常不实现!= as!(左==右)。他们用否定的逻辑完全实现它(如==)。例如,datetime实现==as

1
return d1.InternalTicks == d2.InternalTicks;

而且!= AS

1
return d1.InternalTicks != d2.InternalTicks;

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

1
return !(d1==d2);

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


为了回答你的编辑,关于如果你覆盖了一个,为什么你被强制覆盖两个,这都在继承中。

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

在几乎任何其他方法重写的情况下,假设重写一个方法也会导致反义词方法的行为改变,这是不合逻辑的。如果用increment()和decrement()方法创建了一个类,并在子类中覆盖increment(),那么您希望decrement()也被覆盖行为的另一个对象覆盖吗?在所有可能的情况下,编译器都不能足够聪明地为运算符的任何实现生成一个反函数。

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

真正应该问的问题是为什么<和>以及<=和>=对于比较运算符是成对的,当用数字表示时,这些比较运算符必须同时实现!(a=b和!(a>b)==a<=b.如果覆盖一个,则需要实现全部四个,并且可能需要覆盖==(和!=)同样,因为(a<=b)==(a==b)如果a在语义上等于b。


如果您的自定义类型重载了=,而不是!=然后将由处理!=对象的运算符!=object,因为所有东西都是从object派生的,这与customtype有很大的不同!=自定义类型。

另外,语言创建者可能希望它以这种方式为编码人员提供最大的灵活性,并且这样他们就不会对您打算做的事情做出假设。


我首先想到的是:

  • 如果检验不平等比检验平等快得多怎么办?
  • 在某些情况下,如果您想为==!=同时返回false(也就是说,如果由于某种原因无法进行比较)


你问题的关键词是"为什么"和"必须"。

因此:

回答是这样的,因为他们设计的是这样的,是真的…但不回答你问题的"为什么"部分。

回答说,有时独立地覆盖这两种情况可能会有所帮助,这是真的……但不回答你问题中的"必须"部分。

我认为简单的答案是,没有任何令人信服的理由来解释为什么C要求你忽略两者。

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

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


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

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


在这里再加上一个很好的答案:

考虑一下在调试器中会发生什么,当您尝试单步执行!=运算符并最终使用==运算符时!说说困惑!

clr允许您自由地删除一个或其他操作符是有意义的,因为它必须使用多种语言。但是,有很多不公开clr特性的例子(例如,ref返回和局部变量),以及许多不在clr本身中实现特性的例子(例如:usinglockforeach等)。


简而言之,强制一致性。

"="和"!"='始终是真正的对立面,不管你如何定义它们,通过它们的口头定义"等于"和"不等于"来定义它们。通过只定义其中一个,你会发现一个相等运算符不一致,其中"=='和'!='对于两个给定值都可以为真或都为假。您必须同时定义两者,因为当您选择定义一个时,您还必须适当地定义另一个,以便清楚地知道您对"平等"的定义是什么。编译器的另一个解决方案是只允许您重写"=="或"!='并将另一个保留为固有的否定另一个。显然,C编译器并不是这样的,我相信有一个合理的理由可以完全归因于简单性的选择。

您应该问的问题是:"为什么我需要重写操作符?"这是一个强有力的决定,需要强有力的推理。对于对象,"="和"!='参照比较。如果您要重写它们以不通过引用进行比较,那么您将创建一个一般的运算符不一致性,这对于任何阅读该代码的其他开发人员来说都不明显。如果您试图问"这两个实例的状态是否相等?",然后应该实现IEquattle,定义equals()并使用该方法调用。

最后,iequatable()没有为相同的推理定义notequals():可能会导致相等运算符不一致。noteQuals()应始终返回!相等()。通过将noteQuals()的定义打开到实现equals()的类中,您将再次在确定相等性时强制解决一致性问题。

编辑:这只是我的推理。


编程语言是异常复杂的逻辑语句的语法重组。有鉴于此,你能在不定义不平等的情况下定义一个平等的情况吗?答案是否定的。如果一个物体A等于物体B,那么物体A的倒数不等于B也必须是真的。另一个显示这一点的方法是

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

这为语言确定对象的相等性提供了明确的能力。例如,比较为空!=null可以向不实现非相等语句的相等系统的定义中抛出一个扳手。

现在,关于这个想法!=可替换定义,如

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

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


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

当我超载时,我总是用你的方法。然后我就用在另一个里面。

你说得对,只要做少量的工作,编译器就可以免费给我们这个。