web-dev-qa-db-ja.com

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

C#コンパイラでは、カスタム型が演算子_==_を定義するときは常に、_!=_も定義する必要があります( here を参照)。

どうして?

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

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

等式演算子が定義されており、不等式演算子がコピーと貼り付けであり、すべての比較が逆になり、すべての&&が||に切り替わる、仕上がりの悪いケースを見てきました。 (ポイントを取得します...基本的に!(a == b)De Morganのルールによって展開されます)。これは、Luaの場合のように、コンパイラーが設計によって削除する可能性のある悪い習慣です。

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

[〜#〜] edit [〜#〜]

プログラマーが平等と不平等のチェックを実装できるようにする正当な理由があるようです。答えのいくつかは、それがニースかもしれない場合を指します。

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

_Object.Equals_、_IEquatable.Equals_ _IEqualityComparer.Equals_のような.NETインターフェースの設計選択とは際立った対照もあります。ここで、NotEqualsカウンターパートの欠如はフレームワークが!Equals()等しくないオブジェクト。それだけです。さらに、Dictionaryのようなクラスおよび.Contains()のようなメソッドは、前述のインターフェースにのみ依存し、定義されていても直接演算子を使用しません。実際、ReSharperが等式メンバーを生成する場合、Equals()に関して_==_と_!=_の両方を定義しますが、それでもユーザーが演算子を生成することを選択した場合のみです。フレームワークではオブジェクトの等価性を理解するために等価演算子は必要ありません。

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

340
Stefan Dragnev

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

この基本的な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#で作成することはできませんが、canは.NET用にコンパイルされたクラスを使用します。 ==にオーバーロードされた演算子を使用することは明らかです。それで、ランタイムは!=に何を使用しますか?

C#EMCA標準には、同等性を評価するときに使用する演算子を決定する方法を説明する一連のルール(セクション14.9)があります。単純化しすぎて完全に正確ではないため、比較される型が同じ型である場合andオーバーロードされた等価演算子が存在する場合、標準参照ではなくそのオーバーロードを使用しますObjectから継承された等価演算子。そのため、演算子が1つしか存在しない場合、すべてのオブジェクトが持つデフォルトの参照等価演算子を使用し、それにオーバーロードがないことは驚くことではありません。1

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

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


予期しない動作を防ぐには

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

それに関してはforcingデフォルトの動作に移行する代わりに、オーバーロードすることで、標準(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以外を返す場合、コンパイラcannotは自動的に反対の型を推測します。さらに、演算子does return boolの場合、その特定の1つのケースにのみ存在する生成コード、または上で述べたように非表示にするコードを作成しても意味がありませんCLRのデフォルトの動作。


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

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

多くの開発者は、Visual Basicのように簡単に記述、読み取り、および保守できる言語があればいいのに、それでもC++のパワーと柔軟性を提供したいと思っています。

はい。C#の設計目標は、C++とほぼ同じ量の電力を提供することであり、厳密な型安全性やガベージコレクションなどの利便性のために少しだけ犠牲にします。 C#はC++を強力にモデル化したものです。

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

現在、C++は直接require補完演算子をオーバーロードしません。サンプルプログラムでコードをコンパイルした場合、エラーなしで実行されます。ただし、行を追加しようとした場合:

cout << (a != b);

あなたは得るでしょう

コンパイラエラーC2678(MSVC):バイナリ '!=':タイプ 'Test'の左側のオペランドを受け取る演算子が見つかりません(または、受け入れ可能な変換がありません) `。

したがって、C++自体はペアでオーバーロードする必要はありませんが、しないでは、オーバーロードしていない等値演算子を使用できますカスタムクラス。すべてのオブジェクトにはデフォルトのオブジェクトがあるため、.NETで有効です。 C++はサポートしていません。


1.副次的な注意事項として、C#標準では、どちらか一方をオーバーロードする場合は、演算子のペアをオーバーロードする必要があります。これは、単にcompilerではなく、standardの一部です。ただし、同じ要件を持たない別の言語で記述された.netライブラリにアクセスする場合は、呼び出す演算子の決定に関する同じ規則が適用されます。

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

3.そして、Java、しかしそれは本当にここのポイントではありません

157

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

次の場合があります。

var a = SomeObject();

a == truefalseを返し、a == falsefalseを返します。

48
Yuck

それ以外の多くの領域でC#がC++に従うのは、考えられる最良の説明は、場合によってはわずかに異なるアプローチを取りたいと思うかもしれないということです「平等」を証明するより「平等ではない」を証明すること。

たとえば、文字列比較を使用すると、たとえば、一致しない文字が見つかった場合、ループの外で等価性とreturnをテストできます。ただし、より複雑な問題があるため、それほどきれいではない場合があります。 bloom filter が思い浮かびます。セット内で要素がnotであるかどうかをすぐに判断するのは非常に簡単ですが、要素セット内。同じreturnテクニックを適用できますが、コードはそれほどきれいではないかもしれません。

24
Brian Gordon

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

return d1.InternalTicks == d2.InternalTicks;

および!= as

return d1.InternalTicks != d2.InternalTicks;

あなた(または暗黙的に実行した場合はコンパイラ)として!=を実装する場合

return !(d1==d2);

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

20
hatchet

編集に答えるために、どちらかをオーバーライドする場合に両方をオーバーライドすることを強制される理由に関して、それはすべて継承にあります。

==をオーバーライドすると、何らかのセマンティックまたは構造的な同等性を提供する可能性が高くなります(たとえば、異なるインスタンスであっても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つすべてを実装する必要があります。また、おそらく==(および!=)もオーバーライドする必要があります。これは、aがセマンティック上である場合、(a <= b)==(a == b) bと等しい.

16
KeithS

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

また、言語作成者はおそらく、この方法でコーダーに最も柔軟性を持たせ、またユーザーが何をしようとしているかについての仮定を立てないようにすることを望んでいました。

12
Chris Mullins

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

  • 不平等のテストは、平等のテストよりもはるかに高速ですか?
  • 場合によっては==!=の両方にfalseを返したい場合(つまり、何らかの理由で比較できない場合)
9
Dan Abramov

質問のキーワードは「why」と「must」です。

結果として:

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

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

簡単な答えは、is n't C#の納得できる理由があると思いますrequires両方をオーバーライドする必要があります。

この言語では、==のみをオーバーライドし、!=のデフォルト実装である! thatを提供する必要があります。 !=もオーバーライドしたい場合は、お試しください。

それは良い決断ではなかった。人間は言語を設計し、人間は完全ではありません。C#は完全ではありません。シュラグとQ.E.D.

5

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

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

4
GolezTrol

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

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

CLRを使用すると、多くの言語で動作する必要があるため、1つまたは他の演算子を自由に除外できます。しかし、C#の機能を公開していないC#の例(refリターンやローカルなど)や、CLR自体にない機能の実装例(例:usinglockforeachなど)。

3
Andrew Russell

要するに、強制的な一貫性。

「==」と「!=」は、それらがどのように定義されていても常に真の反対であり、「等しい」と「等しくない」の言葉による定義によって定義されます。いずれか1つを定義するだけで、2つの与えられた値に対して '=='と '!='の両方がtrueまたはfalseになる可能性がある等式演算子の不整合が生じます。どちらか一方を定義することを選択した場合、もう一方も適切に定義する必要があるため、「平等」の定義が何であるかがはっきりとわかるようにする必要があります。コンパイラの他の解決策は、 '==' OR '!='のみをオーバーライドし、他方を本質的に他方を無効のままにすることです。明らかに、そうではありませんC#コンパイラと、単純な選択肢として厳密に起因する正当な理由があると確信しています。

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

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

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

2
Emoire

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

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

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

さて、!=の考え方に関しては、単に

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

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

2
Josh