C#で==と!=の両方を定義する必要があるのはなぜですか?


346

C#コンパイラでは、カスタム型がoperatorを定義する場合は常に==、それも定義する必要があります!=ここを参照)。

どうして?

デザイナーがなぜそれを必要と考えたのか、そしてどちらか一方しか存在しない場合にコンパイラーがどちらかの演算子の妥当な実装をデフォルトにできないのか知りたいです。たとえば、Luaでは等値演算子のみを定義でき、もう1つは無料で取得できます。C#は、==または==と!=の両方を定義して、不足している!=演算子をとして自動的にコンパイルするように要求することで、同じことを行うことができます!(left == right)

(IEEE-754 NaNのように)一部のエンティティが等しくも等しくもない場合がある奇妙なコーナーケースがあることを理解していますが、それらは例外ではなく、規則のように見えます。したがって、これは、C#コンパイラの設計者が例外をルールにした理由を説明していません。

等号演算子が定義されている仕上がりの悪さの例を見てきましたが、不等号演算子は、すべての比較が反転され、すべての&&が||に切り替えられたコピー/貼り付けです。(要点がわかります...基本的に!(a == b)は、ドモルガンのルールによって拡張されます)。これは、Luaの場合のように、コンパイラーが意図的に排除できるのは悪い習慣です。

注:演算子<> <=> =についても同様です。これらを不自然な方法で定義する必要がある場合は想像できません。Luaでは、前者の否定を通して、<と<=だけを定義し、> =と>を自然に定義できます。C#が同じことをしないのはなぜですか(少なくとも「デフォルト」)。

編集

どうやらプログラマーが等しいかどうかのチェックを実装できるようにする正当な理由があるようです。いくつかの回答は、それが良い場合を示しています。

しかし、私の質問の核心は、通常これが論理的に必要ではないのに、なぜこれがC#で強制的に要求されるのかということです。

またObject.Equals、のような.NETインターフェースの設計上の選択とは著しく対照的IEquatable.Equals IEqualityComparer.Equalsです。NotEquals対応するものがないため、フレームワークは!Equals()オブジェクトを不平等と見なし、それがそうです。さらに、のようなクラスDictionaryとメソッドのようなもの.Contains()は、前述のインターフェースにのみ依存しており、たとえ定義されていても、直接演算子を使用しません。実際、ReSharperが同等のメンバーを生成するとき、ReSharperは、ユーザーが演算子を生成することを選択した場合に限り、その両方の観点から、そしてその場合でも、両方==を定義します。オブジェクトの等価性を理解するために、フレームワークでは等価演算子は必要ありません。!=Equals()

基本的に、.NETフレームワークはこれらの演算子を考慮せず、いくつかのEqualsメソッドのみを考慮します。==と!=の両方の演算子をユーザーがタンデムで定義することを要求する決定は、.NETに関する限り、オブジェクトのセマンティクスではなく、純粋に言語設計に関連しています。


24
優れた質問の+1。何の理由もないようです...もちろん、コンパイラは、特に断りのない限り、!= bは!(a == b)に変換できると想定できます。彼らがこれをやろうと決めた理由も知りたいです。
Patrick87

16
これは、投票されてお気に入りに入れられた質問を私が見た中で最速です。
Christopher Currens、2011

26
@Eric Lippertも確かな回答を提供できると思います。
Yuck

17
「科学者は正しい答えを出す人ではなく、正しい質問をする人です。」これが、SEでは質問が奨励される理由です。そしてそれはうまくいきます!
アドリアーノカルネイロ2011

回答:


161

私は言語デザイナーのために話すことはできませんが、私が推論できることから、それは意図的で適切な設計決定であったようです。

この基本的な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から継承された標準の参照等価演算子ではなく、そのオーバーロードを使用します。したがって、演算子が1つしか存在しない場合、すべてのオブジェクトが持つデフォルトの参照等価演算子が使用され、オーバーロードがないことは驚くにあたりません。1

これが事実であることを知って、本当の質問は次のとおりです。なぜこれがこのように設計されたのか、なぜコンパイラーはそれを自分で理解しないのですか?これは設計上の決定ではないと多くの人が言っていますが、特にすべてのオブジェクトにデフォルトの等価演算子があるという事実に関して、私はこのように考えられたと思います。

では、なぜコンパイラは自動的に!=演算子を作成しないのでしょうか?マイクロソフトの誰かがこれを確認しない限り、私は確実に知ることはできませんが、これは事実の推論から判断できるものです。


予期しない動作を防ぐため

おそらく、==同等性をテストするために値の比較を行いたいと思います。ただし、!=参照が等しくない限り、値が等しいかどうかはまったく気になりませんでした。これは、プログラムがそれらを等しいと見なすために、参照が一致するかどうかのみを気にするためです。結局のところ、これは実際にはC#のデフォルトの動作として概説されています(別の言語で記述された.netライブラリの場合のように、両方の演算子がオーバーロードされなかった場合)。コンパイラーが自動的にコードを追加していた場合、準拠しているはずのコードを出力するためにコンパイラーに依存することができなくなりました。特に、作成したコードがC#とCLIの両方の標準の範囲内にある場合、コンパイラーは、自分の動作を変更する非表示のコードを作成しないでください。

デフォルトの動作に行くのではなく、オーバーロードすることを強制するという点で、私はそれが標準(EMCA-334 17.9.2)2にあると断言できます。規格はその理由を明記していません。これは、C#がC ++から多くの動作を借用しているためだと思います。これについて詳しくは、以下を参照してください。


!=and をオーバーライドする場合==、boolを返す必要はありません。

これはもう1つの理由です。C#では、この関数は:

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

これと同じくらい有効です:

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

bool以外のものを返す場合、コンパイラ反対の型を自動的に推測できません。さらに、オペレーターブール値を返す場合、その特定のケースにのみ存在する生成コードを作成することや、前述のように、CLRのデフォルトの動作を非表示にするコードを作成しても意味がありません。


C#はC ++ 3から多くを借りています

C#が導入されたとき、CMSについて述べているMSDNマガジンの記事がありました。

多くの開発者は、Visual Basicのように記述、読み取り、保守が簡単で、しかもC ++の能力と柔軟性を備えた言語が存在することを望んでいます。

はい、C#の設計目標は、C ++とほぼ同じ量のパワーを与えることでした。厳密な型の安全性やガベージコレクションなどの利便性のために少しだけ犠牲にしています。C#はC ++に基づいて強くモデル化されました。

この例のプログラムに示すように、C ++では、等値演算子はboolを返す必要がないことを知って驚かないかもしれません

現在、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ですが、それはここでは本当に重要ではありません


75
boolを返す必要はありません」..それが私たちの答えです。よくやった。!(o1 == o2)が明確に定義されていない場合、標準がそれに依存することは期待できません。
ブライアンゴードン

10
皮肉なことに、論理演算子に関するトピックで、文で二重否定を使用しました。「C#ではどちらか一方のみをオーバーライドできません」は、実際には論理的には「C#ではどちらか一方のみをオーバーライドできる」ことを意味します。
Dan Diplo、2011

13
@Dan Diplo、それは大丈夫です、私がそれをしないことを禁じられているわけではありません。
Christopher Currens、2011

6
trueの場合、#2は正しいと思います。しかし今、問題は次のよう==になります。なぜbool?を返すと、intそれを条件ステートメントとして使用できなくなります。if(a == b)コンパイルされなくなります。ポイントは何ですか?
BlueRaja-Danny Pflughoeft、2011

2
暗黙的にboolに変換されたオブジェクト(演算子のtrue / false)を返すこともできますが、それがどこに役立つかはまだわかりません。
Stefan Dragnev、2011

53

おそらく誰かが3値論理(つまりnull)を実装する必要がある場合のために。このような場合-たとえばANSI標準SQL-入力によっては、演算子を単純に否定することはできません。

次のようなケースが考えられます。

var a = SomeObject();

そして、a == trueリターンfalsea == falseも戻りますfalse


1
以来、私は、その場合には言うだろうaあなたは3ステート列挙型と比較する必要があるブール値ではなく、本物ではないですtruefalse
ブライアンゴードン

18
ちなみに、私は誰の参照信じることはできませんするFileNotFound ..冗談を
ブライアン・ゴードン・

28
場合a == truefalse、私はいつも期待a != trueするtrueにもかかわらず、a == falseであることfalse
FEDE

20
@Fedeが頭に釘を打ちました。 これは本当に質問とは何の関係もありません -あなたは誰かがなぜオーバーライドしたいのかを説明しているのでは==なく、なぜ両方をオーバーライドする必要があるのか​​を説明しているのです。
BlueRaja-Danny Pflughoeft 2011

6
@Yuck-はい、適切なデフォルト実装があります。これは一般的なケースです。あなたの例でさえ、のデフォルトの実装は!=正しいでしょう。で、非常に私たちがしたいと思う珍しいケースx == yx != yの両方であることをfalseに(私は理にかなって、単一の例を考えることはできません)、彼らはまだデフォルトの実装をオーバーライドして、独自に提供することができます。 常に一般的なケースをデフォルトにしてください!
BlueRaja-ダニープフルフフト

25

C#が多くの領域でC ++に依存していることを除けば、私が考えることができる最も良い説明は、「等しくない」を証明することと「等しくない」を証明することとでは少し異なるアプローチを取ることを望む場合があるということです。

たとえば、文字列比較を使用returnすると、一致しない文字が見つかったときに、等価性をテストしてループから外れるだけです。しかし、それはもっと複雑な問題でそれほどきれいではないかもしれません。ブルームフィルタは、頭に浮かぶ。それは要素がある場合はすぐに伝えることは非常に簡単ですではない要素があれば教えてセットではなく、困難であるセットに。同じreturnテクニックを適用できますが、コードはそれほどきれいではないかもしれません。


6
素晴らしい例。私はあなたが理由を理解していると思います。単純な例!では、等式の単一の定義に固定されます。情報に基づいたコーダーがとの両方 を最適化できる場合が==あり!=ます。
Yuck

13
したがって、これは、なぜ強制する必要があるのか​​ではなく、両方をオーバーライドするオプションを与える必要がある理由を説明しています。
BlueRaja-Danny Pflughoeft、2011

1
そのため、両方を最適化する必要はなく、両方の明確な定義提供する必要があります。
Jesper

22

.netソースで==および!=のオーバーロードの実装を見ると、!=を!(left == right)として実装していないことがよくあります。彼らはそれを完全に(==のように)否定論理で実装します。たとえば、DateTimeは==を次のように実装します

return d1.InternalTicks == d2.InternalTicks;

そして!= as

return d1.InternalTicks != d2.InternalTicks;

あなた(またはそれを暗黙的に行った場合はコンパイラ)が!=を

return !(d1==d2);

次に、クラスが参照しているものの==および!=の内部実装について仮定します。その仮定を回避することが彼らの決定の背後にある哲学かもしれません。


17

あなたの編集に答えるために、一方をオーバーライドすると両方をオーバーライドする必要がある理由に関しては、すべて継承にあります。

==をオーバーライドすると、何らかの意味的または構造的な同等性が提供される可能性が最も高くなります(たとえば、InternalTicksプロパティが異なる場合でもDateTimesが等しい場合)、演算子のデフォルトの動作を変更します。すべての.NETオブジェクトの親であるオブジェクト。==演算子は、C#ではメソッドであり、その基本実装Object.operator(==)は参照比較を実行します。Object.operator(!=)は、別の異なるメソッドであり、参照比較も実行します。

メソッドをオーバーライドする他のほとんどすべてのケースでは、1つのメソッドをオーバーライドすると、反則的なメソッドへの動作が変更されると推定することは不合理です。Increment()およびDecrement()メソッドを使用してクラスを作成し、子クラスでIncrement()をオーバーライドした場合、オーバーライドされた動作の反対でDecrement()もオーバーライドされることを期待しますか?可能性のあるすべての場合において、コンパイラーは、演算子の実装用の逆関数を生成するほどスマートにすることができません。

ただし、演​​算子はメソッドと非常によく似ていますが、概念的にはペアで機能します。==および!=、<および>、および<=および> =。この場合、!=が==とは異なる動作をすることを消費者の観点から考えるのは非論理的です。したがって、コンパイラーはすべてのケースでa!= b ==!(a == b)であると想定することはできませんが、==と!=は同じように動作するはずなので、コンパイラーはペアで実装する必要がありますが、実際にはそれを行うことになります。クラスの場合、a!= b ==!(a == b)の場合、!(==)を使用して!=演算子を実装するだけですが、そのルールがオブジェクトに対してすべての場合に適用されない場合(たとえば、等しいまたは等しくない特定の値との比較が無効な場合)、IDEよりも賢くする必要があります。

尋ねられるべきREALの質問は、<と>および<=と> =が比較演算子のペアであり、数値で表すと!(a <b)== a> = bと!(a> b)== a <= b。1つをオーバーライドする場合は4つすべてを実装する必要があり、==(および!=)もオーバーライドする必要がある可能性があります。 bに等しい。


14

!=ではなく、カスタムタイプの==をオーバーロードすると、すべてがオブジェクトから派生するため、オブジェクト!=オブジェクトの!=演算子によって処理されます。これは、CustomType!= CustomTypeとは大きく異なります。

また、言語の作成者はおそらく、この方法でプログラマーに最大の柔軟性を与えることを望んでおり、プログラマーが何をしようとしているのかを想定していません。



2
柔軟性の理由は、たとえばblogs.msdn.com/b/ericlippert/archive/2011/06/23/…など、除外することを決定した非常に多くの機能にも適用できます。したがって、等値演算子の実装。
Stefan Dragnev

それはとても興味深いです。便利な機能のようです。彼らがなぜそれを省いたのか私にはわかりません。
Chris Mullins、2011

11

これが最初に頭に浮かぶものです。

  • 不等式のテストが等式のテストよりもはるかに速い場合はどうなりますか?
  • どのようないくつかのケースであれば、あなたが返すようにしたいfalseの両方を==して!=(つまり、彼らが何らかの理由で比較できない場合)

5
最初のケースでは、不等式テストに関して等式テストを実装できます。
Stefan Dragnev 2011

後者の場合は次のような構造を壊すだろうDictionaryし、Listあなたがそれらをあなたのカスタム型を使用している場合。
Stefan Dragnev、2011

2
@Stefan:演算子ではなくとをDictionary使用GetHashCodeEqualsます。List使用しEqualsます。
Dan Abramov、2011

6

まあ、それはおそらく単なる設計上の選択ですが、あなたが言うx!= yように、と同じである必要はありません!(x == y)。デフォルトの実装を追加しないことにより、特定の実装を実装することを忘れることはできません。そして、それが本当にあなたの言うように取るに足りないものである場合は、一方をもう一方を使用して実装することができます。これが「練習不足」だとは思いません。

C#とLuaの間にも他の違いがあるかもしれません...


1
これは、一方を他方の観点から実装する優れたプラクティスです。私はそれが他の方法で行われたのを見ました-これはエラーが発生しやすいです。
Stefan Dragnev、2011

3
それはC#の問題ではなく、プログラマーの問題です。この権利の実装に失敗したプログラマは、他の分野でも失敗するでしょう。
GolezTrol 2011

@GolezTrol:そのLuaの参照について説明してもらえますか?私はLuaにまったく精通していませんが、あなたの参照は何かを示唆しているようです。
zw324 2011

@Ziyao Wei:OPは、LuaがこれをC#とは異なる方法で行うことを指摘しています(Luaは明らかに、言及された演算子のデフォルトの対応物を追加します)。そのような比較を単純化しようとしています。違いがまったくない場合、少なくとも1つは存在する権利がありません。ルアも知らないと言っておく必要があります。
GolezTrol 2011

6

あなたの質問のキーワードは、「なぜ」と「必ず」です。

結果として:

彼らがそうするように設計したので、このように答えるのは本当です...しかし、あなたの質問の「なぜ」の部分には答えません。

これらの両方を個別にオーバーライドすると役立つ場合があると答えることは事実ですが、質問の「必須」の部分には答えません。

簡単な答えは、C#で両方をオーバーライドする必要があるという説得力のある理由はないということです

言語はのみをオーバーライドできるようにし==!=そのデフォルトの実装を提供する必要があります!。オーバーライドしたい場合は、それを試してください!=

それは良い決断ではありませんでした。人間は言語を設計し、人間は完璧ではなく、C#は完璧ではありません。肩をすくめてQED


5

ここで優れた答えに追加するだけです:

演算子にステップインして、代わりに演算子で終了しようとすると、デバッガーで何が起こるかを考えてください!混乱について話してください!!===

CLRを使用すると、多くの言語で機能する必要があるため、いずれかの演算子を自由に除外できるようになります。しかし、CLRを露出していないC#の例がたくさんあります(特徴ref戻り、地元の人々 、例えば)、およびないCLR自体に機能を実装する例の多く(例:usinglockforeach、など)。


3

プログラミング言語は、非常に複雑な論理ステートメントを構文的に再配置したものです。それを念頭に置いて、不平等のケースを定義せずに平等のケースを定義できますか?答えはノーだ。オブジェクトaがオブジェクトbと等しい場合、オブジェクトaの逆がbと等しくないことも真でなければなりません。これを示す別の方法は

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

これにより、言語がオブジェクトの同等性を決定する明確な機能が提供されます。たとえば、比較NULL!= NULLは、不等式ステートメントを実装しない等式システムの定義にレンチをスローする可能性があります。

ここで、!=の考えに関しては、単に次のように置き換え可能な定義である

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

私はそれについて議論することはできません。ただし、プログラマーがオブジェクトの等価性と非等価性を一緒に明示的に定義することを余儀なくされたのは、おそらくC#言語仕様グループによる決定でした


Nullnullはへの有効な入力であるため、問題になるだけですが==、そうすることはできません。個人的には、それが膨大な量のあいまいでずさんなプログラミングを可能にするだけだと思います。
nicodemus13 14年

2

つまり、強制整合性です。

'=='と '!='は、「等しい」と「等しくない」の口頭による定義で定義されているため、どのように定義しても常に正反対です。それらの1つだけを定義することにより、与えられた2つの値に対して '=='と '!='の両方がtrueまたは両方ともfalseになる可能性がある等号演算子の不整合が発生します。どちらかを定義する必要があるので、どちらか一方を適切に定義して、「平等」の定義が露骨に明確になるようにする必要があります。コンパイラーのもう1つの解決策は、 '==' OR '!='のオーバーライドのみを許可し、他のものは本質的に他のものを無効にすることです。明らかに、C#コンパイラではそうではなく、確かに

あなたが尋ねる必要がある質問は、「なぜ演算子をオーバーライドする必要があるのですか?」です。それは強い推論を必要とする強い決断です。オブジェクトの場合、「==」と「!=」は参照によって比較されます。参照によって比較しないようにそれらをオーバーライドする場合は、そのコードを熟読する他の開発者には明らかではない、一般的な演算子の不整合を作成しています。「これら2つのインスタンスの状態は同等ですか?」という質問をしようとしている場合は、IEquatibleを実装し、Equals()を定義して、そのメソッド呼び出しを利用する必要があります。

最後に、IEquatable()は、同じ理由でNotEquals()を定義していません。等価演算子の不整合が生じる可能性があります。NotEquals()は常に!Equals()を返す必要があります。NotEquals()の定義をEquals()を実装するクラスに公開することにより、等価性の決定に一貫性の問題を再び強制することになります。

編集:これは単に私の推論です。


これは非論理的です。理由が一貫性のある場合は、反対のことが当てはまります。実装できるのoperator ==はコンパイラーだけであり、コンパイラーが推測しoperator !=ます。結局のところ、これはそれがために何をするかであるoperator +operator +=
Konrad Rudolph

1
foo + = bar; 複合代入であり、foo = foo + bar;の省略形です。オペレーターではありません。
ブレンダーフリーキー

実際、それら「常に真の反対」である場合、それが自動的に… として定義される理由なります(これはC#では行われません)。a != b!(a == b)
andrewf

-3

おそらく、彼らが思いもしなかった何かだけをする時間はありませんでした。

==をオーバーロードするときは、常にあなたのメソッドを使用します。それから私はそれを他のもので使用します。

ほんの少しの作業で、コンパイラはこれを無料で私たちに提供することができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.