演算子==はC#のジェネリック型に適用できませんか?


326

MSDN==オペレーターのドキュメントによると、

定義済みの値タイプの場合、等価演算子(==)は、そのオペランドの値が等しい場合はtrueを返し、それ以外の場合はfalseを返します。文字列以外の参照型の場合、2つのオペランドが同じオブジェクトを参照している場合、==はtrueを返します。文字列型の場合、==は文字列の値を比較します。ユーザー定義の値タイプは、==演算子をオーバーロードできます(演算子を参照)。したがって、ユーザー定義の参照型も使用できますが 、既定では==は、定義済みの参照型とユーザー定義の参照型の両方について上記のように動作します。

では、なぜこのコードスニペットはコンパイルに失敗するのでしょうか。

bool Compare<T>(T x, T y) { return x == y; }

演算子 '=='をタイプ 'T'および 'T'のオペランドに適用できないというエラーが表示されます。私が理解している限り、==演算子はすべてのタイプに対して事前定義されているので、なぜだろうか。

編集:ありがとう、みんな。最初はステートメントが参照型のみに関するものであることに気付きませんでした。また、ビットごとの比較はすべての値の型に対して提供されると思っていましたが、これ正しくないことがわかりました。

しかし、参照型を使用している場合、==演算子は定義済みの参照比較を使用するのでしょうか、それとも型が定義されている場合は、演算子のオーバーロードバージョンを使用するのでしょうか。

編集2:試行錯誤の==結果、無制限のジェネリック型を使用する場合、オペレーターは事前定義された参照比較を使用することがわかりました。実際、コンパイラーは、制限された型の引数に対して見つけることができる最良の方法を使用しますが、それ以上は調べません。たとえば、以下のコードはtrueTest.test<B>(new B(), new B())が呼び出された場合でも、常にを印刷します。

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }

フォローアップの質問に対する回答については、私の回答をもう一度参照してください。
Giovanni Galbo

ジェネリックがなくても==、同じ型の2つのオペランドの間で許可されない型がいくつかあることを理解することは有用かもしれません。これはstruct、をオーバーロードしないタイプ(「事前定義された」タイプを除く)に当てはまりますoperator ==。簡単な例として、この試してください:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
ジャップスティグ・ニールセン

私自身の古いコメントを続けます。たとえば(他のスレッドを参照)、を使用するとvar kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;、は構造体であり、C#の事前定義された型ではないkvp1 == kvp2ためKeyValuePair<,>、チェックできませんoperator ==。しかし、var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;あなたができない例が示されていますe1 == e2(ここでは、オーバーロードしないネストされた構造体List<>.Enumerator"List`1+Enumerator[T]"ランタイムによって呼び出されます)があります==)。
Jeppe Stig Nielsen 2017

RE:「なぜこのコードスニペットはコンパイルに失敗するのですか?」- boolvoid
えーと

1
@ BrainSlugs83 10年前のバグをキャッチしてくれてありがとう!
Hosam Aly

回答:


143

「...デフォルトでは、==は、事前定義された参照タイプとユーザー定義の参照タイプの両方について、上記のように動作します。」

タイプTは必ずしも参照タイプではないため、コンパイラーはその仮定を行うことができません。

ただし、これはより明示的であるためコンパイルされます。

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

「しかし、参照タイプを使用している場合、==演算子は事前定義された参照比較を使用しますか、それともタイプが定義されている場合、オーバーロードされたバージョンの演算子を使用しますか?」

Genericsの==はオーバーロードされたバージョンを使用すると考えていましたが、次のテストはそれ以外のことを示しています。興味深い...なぜか知りたい!誰かが知っている場合は共有してください。

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

出力

インライン:オーバーロード==呼び出されました

一般:

何かキーを押すと続行します 。。。

フォローアップ2

比較方法を次のように変更することを指摘しておきます

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

オーバーロードされた==演算子が呼び出されます。タイプを(whereとして)指定しないと、コンパイラーはオーバーロードされた演算子を使用する必要があると推測できません...タイプを指定しなくても、その決定を行うのに十分な情報があると思います。


ありがとう。ステートメントが参照型のみに関するものであることに気付きませんでした。
Hosam Aly

4
再:フォローアップ2:実際には、コンパイラーは見つけた最適なメソッド(この場合はTest.op_Equal)をリンクします。ただし、Testから派生して演算子をオーバーライドするクラスがある場合、Testの演算子は引き続き呼び出されます。
Hosam Aly

4
指摘したいのは、オーバーライドされたEqualsメソッド内では常に(==演算子ではなく)実際の比較を行う必要があるということです。
jpbochi 2009

11
オーバーロードの解決はコンパイル時に行われます。したがって、==ジェネリック型Tとの間にある場合T、どの制約が適用されるかを考慮して、最適なオーバーロードが見つかりTます(これに対して値型がボックス化されないという特別なルールがあります(これは意味のない結果になります)。したがって、参照タイプであることを保証するいくつかの制約)。あなたにフォローアップ2あなたが来る場合は、DerivedTestオブジェクト、およびDerivedTest派生からTestしかし、新しいオーバーロードを導入し==、再度「問題」を持つことになります。どのオーバーロードが呼び出されるかは、コンパイル時にILに「焼き付け」られます。
Jeppe Stig Nielsen 2013

1
奇妙なことに、これは一般的な参照タイプ(この比較が参照の等価性であることが期待される)で機能するようですが、文字列でも参照の等価性を使用しているようです-したがって、2つの同一の文字列を比較して==(クラス制約のあるジェネリックメソッド)は異なると言います。
JonnyRaa 2013年

292

他の人が言ったように、Tが参照型になるように制約されている場合にのみ機能します。制約がなければ、nullと比較できますが、nullのみです。null以外の値の型の場合、その比較は常にfalseになります。

Equalsを呼び出す代わりに、IComparer<T>- を使用することをお勧めします。これ以上情報がない場合EqualityComparer<T>.Defaultは、適切な選択です。

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

他のものを除いて、これはボクシング/キャスティングを避けます。


ありがとう。単純なラッパークラスを作成しようとしていたため、実際のラップされたメンバーに操作を委任したいと思っていました。しかし、EqualityComparer <T> .Defaultを知っていることは確かに私に付加価値をもたらしました。:)
Hosam Aly

さておき、ジョン。あなたは私の投稿にコメントre pobox vs yodaを書きたいと思うかもしれません。
マークグラベル

4
EqualityComparer <T>の使用に関する
良いヒント

1
+1は、nullと比較できること、およびnullを許容しない値型の場合は常にfalseであることを示します
Jalal Said

@BlueRaja:はい、nullリテラルとの比較には特別なルールがあるためです。したがって、「制約がなければ、nullと比較できますが、nullのみです」。それはすでに答えにあります。では、なぜこれが正しくないのでしょうか?
Jon Skeet

41

一般的にEqualityComparer<T>.Default.Equalsは、を実装するものIEquatable<T>、または賢明なEquals実装を持つもので作業を行う必要があります。

しかし、場合==Equals、何らかの理由で別々に実装され、その後、上の私の仕事の一般的な演算子は有用であろう。それは(とりわけ)のオペレーターバージョンをサポートしています:

  • Equal(T value1、T value2)
  • NotEqual(T値1、T値2)
  • GreaterThan(T値1、T値2)
  • LessThan(T値1、T値2)
  • GreaterThanOrEqual(T value1、T value2)
  • LessThanOrEqual(T値1、T値2)

非常に興味深いライブラリ!:)(補足:www.yoda.arachsys.comへのリンクを使用することをお勧めします。poboxが職場のファイアウォールによってブロックされたためですか?他の人が同じ問題に直面する可能性があります。)
Hosam Aly

考えはあるpobox.com/~skeetます常に、それは別の場所に移動した場合でも-私のウェブサイトを指します。私は後世のためにpobox.com経由でリンクを投稿する傾向がありますが、現在は代わりにyoda.arachsys.comを使用できます。
Jon Skeet

pobox.comの問題は、それがWebベースの電子メールサービス(または会社のファイアウォールによる)であるため、ブロックされることです。そのため、リンクをたどることができませんでした。
Hosam Aly

「しかし、==とEqualsが何らかの理由で異なる方法で実装されている場合」-聖なる煙!しかしなんと!多分私は逆にユースケースを見る必要があるだけですが、発散の等しいセマンティクスを持つライブラリは、ジェネリックの問題よりも大きな問題に遭遇する可能性があります。
Edward Brey 2017

@EdwardBreyあなたは間違っていません。コンパイラがそれを強制できればいいのですが...
Marc Gravell

31

多くの答えがあり、1つではないのでなぜその理由が説明されますか?(ジョヴァンニが明示的に尋ねた)...

.NETジェネリックはC ++テンプレートのように機能しません。C ++テンプレートでは、実際のテンプレートパラメータが判明した後でオーバーロードの解決が行われます。

.NETジェネリック(C#を含む)では、オーバーロードの解決は、実際のジェネリックパラメーターを知らなくても発生します。コンパイラーが呼び出す関数を選択するために使用できる唯一の情報は、総称パラメーターの型制約から得られます。


2
しかし、コンパイラがそれらを汎用オブジェクトとして処理できないのはなぜですか?結局のところ==、それが参照型でも値型でも、すべての型で機能します。それはあなたが答えたとは思えない質問です。
nawfal 2013

4
@nawfal:実際にはあり==ません。すべての値の型に対して機能するわけではありません。さらに重要なことは、すべてのタイプで同じ意味を持つわけではないため、コンパイラーはそれをどうするかを認識していません。
Ben Voigt 2013

1
ベン、ああ、そう==です。作成せずに作成できるカスタム構造体がありませんでした。私はここでの主なポイントだと思うようにあなたの答えには、あまりにもその部分を含めることができます
nawfal

12

コンパイルは、Tが構造体(値型)である可能性がないことを認識できません。だからあなたはそれが私が考える参照タイプのものだけであることができることを言わなければなりません:

bool Compare<T>(T x, T y) where T : class { return x == y; }

これは、Tが値型である可能性がx == yある場合、型が演算子==が定義されていない場合など、形式が正しくない場合があるためです。これについても同じことが、より明白になります。

void CallFoo<T>(T x) { x.foo(); }

関数fooを持たないT型を渡すことができるため、これも失敗します。C#では、可能なすべての型が常に関数fooを持っていることを確認する必要があります。それはwhere句によって行われます。


1
説明をありがとう。値の型がそのままでは==演算子をサポートしていないことを知りませんでした。
Hosam Aly

1
Hosam、私はgmcs(モノ)でテストしました、そしてそれは常にリファレンスを比較します。(つまり、オプションで定義されたoperator ==をTに使用していません)
Johannes Schaub-litb 2008

このソリューションには注意点が1つあります。operator==はオーバーロードできません。このStackOverflowの質問を参照してください
Dimitri C.

8

クラス制約なしのようです:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

演算子でclass制約さEqualsれている間は==から継承しObject.Equals、構造体ではがオーバーライドすることを理解する必要がありますValueType.Equals

ご了承ください:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

同じコンパイラエラーも出力します。

まだ、値型の等価演算子の比較がコンパイラによって拒否される理由がわかりません。ただし、これが機能することは知っています。

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}

私は完全なc#noobを知っています。しかし、コンパイラが何をすべきかわからないので失敗すると思います。Tはまだわかっていないため、値の型が許可されるかどうかは、T型によって異なります。参照の場合、参照はTに関係なく単に比較されます。.Equalsを実行すると、.Equalが呼び出されます。
Johannes Schaub-litb 2008

ただし、値型に対して==を実行する場合、値型はその演算子を必ずしも実装する必要はありません。
Johannes Schaub-litb 2008

litb :)ユーザー定義の構造体が==をオーバーロードしない可能性があるため、コンパイラは失敗します。
Jon Limjap 2008

2
最初の比較メソッドは使用せObject.Equals、代わりに参照の等価性をテストします。たとえばCompare("0", 0.ToString())、引数は個別の文字列への参照になるため、falseを返します。これらの文字列はどちらも唯一の文字としてゼロを持っています。
スーパーキャット2013年

1
最後の1つについての小さな落とし穴-構造体に制限していないので、NullReferenceException可能性があります。
Flynn1179 2017

6

私の場合は、等価演算子を単体テストしたかったのです。ジェネリック型を明示的に設定せずに、等価演算子の下でコードを呼び出す必要がありました。のアドバイスはEqualityComparerEqualityComparer呼び出されたEqualsメソッドとしては役に立ちませんでしたが、等価演算子ではありませんでした。

これが、を作成することで、ジェネリック型を処理する方法LINQです。==and !=演算子の適切なコードを呼び出します。

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}

4

このためのMSDN Connectエントリがあります。

Alex Turnerの返答は次のように始まります。

残念ながら、この動作は仕様によるものであり、値の型を含む可能性のある型パラメーターで==を使用できるようにする簡単な解決策はありません。


4

カスタムタイプの演算子が呼び出されることを確認したい場合は、リフレクションを介して呼び出すことができます。ジェネリックパラメーターを使用して型を取得し、目的の演算子(たとえば、op_Equality、op_Inequality、op_LessThan ...)のMethodInfoを取得するだけです。

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

次に、MethodInfoのInvokeメソッドを使用して演算子を実行し、オブジェクトをパラメーターとして渡します。

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

これにより、オーバーロードされた演算子が呼び出され、ジェネリックパラメーターに適用された制約によって定義された演算子は呼び出されません。実用的ではないかもしれませんが、いくつかのテストを含むジェネリック基本クラスを使用する場合に、オペレーターのユニットテストに役立ちます。


3

最新のmsdnを見ながら以下の関数を書いた。2つのオブジェクトxを簡単に比較し、y

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}

4
あなたはブール値と書き込みを取り除くことができますreturn ((IComparable)(x)).CompareTo(y) <= 0;
codidact.comに移動aloisdg

1

bool Compare(T x, T y) where T : class { return x == y; }

ユーザー定義の参照型の場合は==が処理されるため、上記は機能します。
値タイプの場合、==をオーバーライドできます。その場合、「!=」も定義する必要があります。

それが理由かもしれないと思います、それは "=="を使用した一般的な比較を許可しません。


2
ありがとう。参照型も演算子をオーバーライドできると思います。しかし、失敗の理由は今や明らかです。
Hosam Aly

1
==トークンは、二つの異なる事業者に使用されます。指定されたオペランドタイプに対して、等価演算子の互換性のあるオーバーロードが存在する場合、そのオーバーロードが使用されます。それ以外の場合、両方のオペランドが相互に互換性のある参照タイプである場合、参照比較が使用されます。Compare上記のメソッドでは、コンパイラは最初の意味が当てはまることはわかりませんが、2番目の意味が当てはまることを伝えることができるため、等価チェック演算子がオーバーロードされている場合でも(==トークンがtypeの場合など、トークンは後者を使用します。TString
スーパーキャット2013年

0

.Equals()私の作品TKeyはジェネリックタイプです。

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}

それはx.Id.Equals、ではありませんid.Equals。おそらく、コンパイラはのタイプについて何か知っていxます。
Hosam Aly、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.