1. ホーム
  2. c#

[解決済み】なぜC#では==と!=の両方を定義しなければならないのか?

2022-04-14 08:59:49

質問

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

なぜ?

設計者がなぜこれを必要と考えたのか、そしてなぜコンパイラは、どちらか一方の演算子しか存在しないときに、もう一方の演算子の合理的な実装をデフォルトにすることができないのか、興味があるところです。例えば、Luaでは等号演算子だけを定義すれば、もう一方は無料で手に入ります。C#も同じように、「==」または「==」と「!=」の両方を定義するよう求めて、足りない「!=」演算子を次のように自動的にコンパイルすることができます。 !(left == right) .

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

等式演算子が定義された後、不等式演算子がコピーペーストで、比較はすべて逆、&&はすべて||に切り替えられている(要は、ド・モルガンのルールで !(a==b) を展開したもの)ようなお粗末なケースも見たことがあります。これは、Luaのようにコンパイラが設計上排除することができる悪い習慣です。

注 演算子 < > <= >= も同様で、不自然な形で定義する必要があるケースは考えられません。Luaでは、<と<=だけを定義でき、>=と>は、フォーマーの否定によって自然に定義されます。なぜC#は同じことをしないのでしょうか(少なくとも「デフォルトで」)。

EDIT

どうやら、プログラマが好きなように等号や不等号のチェックを実装できるようにする正当な理由があるようです。いくつかの回答は、それが良いケースであることを指摘しています。

しかし、私の疑問の核心は、なぜC#の場合、このようなことが強制的に要求されるのかということです。 通常 ではありません。 論理的に 必要ですか?

のような.NETインターフェースのデザイン選択とは対照的でもあります。 Object.Equals , IEquatable.Equals IEqualityComparer.Equals がないところは NotEquals の対になるものは、フレームワークが !Equals() オブジェクトは不平等である、というだけのことです。さらに、以下のようなクラスは Dictionary のようなメソッドと .Contains() は前述のインターフェースにのみ依存し、演算子が定義されていても直接使用することはありません。実際、ReSharperが等式メンバを生成するときは、この2つの演算子を定義しています。 ==!= という点では Equals() その場合でも、ユーザーが演算子を生成することを選択した場合のみです。等号演算子は、フレームワークがオブジェクトの等号を理解するために必要なものではありません。

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

解決方法は?

言語設計者のことはわかりませんが、私が推論したところでは、意図的で適切な設計判断だったように思います。

この基本的な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++から多くの動作を借りていることに起因すると思います。 これについては、以下を参照してください。


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

これも考えられる理由です。 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#が登場したとき、MSDNマガジンにC#の話をする、書いた記事がありました。

<ブロッククオート

Visual Basicのように書きやすく、読みやすく、メンテナンスしやすく、それでいてC++のようなパワーと柔軟性を備えた言語があれば、と多くの開発者が思っています。

C#の設計目標は、C++とほぼ同等のパワーを持ちながら、厳密な型安全性やガベージコレクションなどの利便性をほんの少し犠牲にすることだったんですね。 C#はC++を強く意識して設計されています。

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

さて、C++は直接 が必要です。 をオーバーロードする必要があります。 サンプルプログラムのコードをコンパイルしてみると、何のエラーもなく実行できることがわかります。 しかし、試しに次の行を追加してみると

cout << (a != b);

を得ることになります。

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

つまり、C++自体はペアでオーバーロードすることを求めてはいないものの はしません。 カスタムクラスでオーバーロードしていない等式演算子を使用することができます。 .NETでは、すべてのオブジェクトにデフォルトのものがあるので有効ですが、C++ではそうではありません。


1. 余談ですが、C#の標準では、どちらかの演算子をオーバーロードしたい場合、ペアの演算子をオーバーロードする必要があるのは変わりません。 これは 標準 であり、単に コンパイラ . しかし、どの演算子を呼び出すかの決定については、同じ要件を持たない他の言語で書かれた.netライブラリにアクセスする場合にも同じ規則が適用されます。

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

3. そしてJava、しかしそれはここでは本当に重要ではありません