首页 文章

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

提问于
浏览
333

C#编译器要求每当自定义类型定义operator == 时,它还必须定义 != (参见here) .

为什么?

对于任何一个运算符,当只有另一个运算符存在时,编译器默认为合理的实现 . 例如,Lua允许您仅定义相等运算符,而您可以免费获得另一个运算符 . C#也可以通过要求你定义==或者两者==和!=然后自动编译缺少的!=运算符作为 !(left == right) 来做同样的事情 .

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

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

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

EDIT

显然有正当理由允许程序员实现对他们喜欢的平等和不平等的检查 . 一些答案指出了可能很好的情况 .

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

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

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

13 回答

  • 151

    我不能代表语言设计师,但从我的理由来看,似乎是有意的,适当的设计决策 .

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

    知道这种情况,真正的问题是:为什么这样设计,为什么编译器不能自己解决?很多人都说这不是一个设计决定,但我喜欢认为它是这样考虑的,特别是关于所有对象都有默认的相等运算符的事实 .

    那么,为什么编译器不能自动创建 != 运算符呢?除非微软的某人确认这一点,否则我无法确定,但这是我可以通过推理事实来确定的 .


    防止意外行为

    也许我想在 == 上进行值比较以测试相等性 . 然而,当它来到 != 时,我没有't care at all if the values were equal unless the reference was equal, because for my program to consider them equal, I only care if the references match. After all, this is actually outlined as default behavior of the C# (if both operators were not overloaded, as would be in case of some .net libraries written in another language). If the compiler was adding in code automatically, I could no longer rely on the compiler to output code that should is compliant. The compiler should not write hidden code that changes the behavior of yours, especially when the code you'写入了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中, equality operators do not have to return bool ,如this example program所示

    现在,C并不直接要求您重载补充运算符 . 如果您在示例程序中编译了代码,您将看到它运行时没有错误 . 但是,如果您尝试添加该行:

    cout << (a != b);
    

    你会得到

    编译器错误C2678(MSVC):binary'!=':找不到运算符,它接受类型为'Test'的左手操作数(或者没有可接受的转换)` .

    因此,虽然C本身并不要求你成对重载,但是 will not 允许你使用一个在.NET中有效的相等运算符,因为所有对象都有一个默认运算符 . C没有 .


    1.作为旁注,如果你想要重载任何一个操作符,C#标准仍然要求你重载这对操作符 . 这是标准的一部分,而不仅仅是编译器 . 但是,当您访问以不同语言编写的.net库时,有关确定调用哪个运算符的相同规则适用 .

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

    和Java一样,但这不是重点

  • -2

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

    你可以有一个案例:

    var a = SomeObject();
    

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

  • 11

    除了C#在很多方面都遵循C之外,我能想到的最好的解释是,在某些情况下,你可能想采用一种稍微不同的方法来证明"not equality"而不是证明"equality" .

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

  • 3

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

    return d1.InternalTicks == d2.InternalTicks;
    

    和!= as

    return d1.InternalTicks != d2.InternalTicks;
    

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

    return !(d1==d2);
    

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

  • 5

    要回答你的编辑,关于为什么你被强制覆盖两者,如果你覆盖一个,它都是继承 .

    如果覆盖==,最有可能提供某种语义或结构相等性(例如,如果DateTimes的InternalTicks属性相等,即使它们可能是不同的实例,它们也相等),那么您正在改变运算符的默认行为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 .

  • 16

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

    此外,语言创建者可能希望以这种方式为编码人员提供最大的灵活性,并且他们也不会对您打算做什么做出假设 .

  • 9

    这是我首先想到的:

    • 如果 testing inequality is much faster than testing equality? 怎么办

    • 如果在某些情况下 you want to return false both for == and != (即如果因某些原因无法进行比较,该怎么办)

  • 4

    你问题中的关键词是“ why " and " must ” .

    结果是:

    以这种方式回答它是因为他们设计得如此,这是真的......但没有回答你问题的“为什么”部分 .

    回答它有时可能有助于独立地覆盖这两者,这是真的......但不回答你问题的“必须”部分 .

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

    该语言应该允许您仅覆盖 == ,并为您提供 != 的默认实现 != . 如果你碰巧想要覆盖 != ,那就去吧 .

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

  • 2

    好吧,它可能只是一个设计选择,但正如你所说, x!= y 不必与 !(x == y) 相同 . 通过不添加默认实现,您可以确定不会忘记实现特定实现 . 如果它's indeed as trivial as you say, you can just implement one using the other. I don't看到这是怎么回事'poor practise' .

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

  • 47

    只是为了增加这里的优秀答案:

    考虑一下 debugger 会发生什么,当你试图进入 != 运算符并最终进入 == 运算符时!谈论混乱!

    CLR允许你自由地遗漏一个或另一个操作符是有道理的 - 因为它必须与许多语言一起使用 . 但是有很多C#的例子没有公开CLR功能(例如 ref 返回和本地),还有很多实现不在CLR本身的功能的例子(例如: usinglockforeach 等) .

  • 20

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

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

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

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

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

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

  • 23

    简而言之,强制一致性 .

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

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

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

    编辑:这只是我的推理 .

  • 2

    可能只是他们没想到的东西没有时间去做 .

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

    你是对的,通过少量的工作,编译器可以免费提供给我们 .

相关问题