Kann der Operator == nicht auf generische Typen in C # angewendet werden?

326

Gemäß der Dokumentation des ==Betreibers in MSDN ,

Für vordefinierte Werttypen gibt der Gleichheitsoperator (==) true zurück, wenn die Werte seiner Operanden gleich sind, andernfalls false. Für andere Referenztypen als Zeichenfolge gibt == true zurück, wenn sich die beiden Operanden auf dasselbe Objekt beziehen. Für den Zeichenfolgentyp vergleicht == die Werte der Zeichenfolgen. Benutzerdefinierte Werttypen können den Operator == überladen (siehe Operator). Dies gilt auch für benutzerdefinierte Referenztypen, obwohl sich == standardmäßig wie oben für vordefinierte und benutzerdefinierte Referenztypen beschrieben verhält.

Warum kann dieses Code-Snippet nicht kompiliert werden?

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

Ich erhalte den Fehler Operator '==' kann nicht auf Operanden vom Typ 'T' und 'T' angewendet werden . Ich frage mich warum, da der ==Operator meines Wissens für alle Typen vordefiniert ist.

Edit: Danke an alle. Ich habe zunächst nicht bemerkt, dass es sich bei der Aussage nur um Referenztypen handelt. Ich dachte auch, dass ein bitweiser Vergleich für alle Werttypen bereitgestellt wird, von denen ich jetzt weiß, dass sie nicht korrekt sind.

==Wenn ich jedoch einen Referenztyp verwende, würde der Operator den vordefinierten Referenzvergleich verwenden oder würde er die überladene Version des Operators verwenden, wenn ein Typ einen definiert?

Bearbeiten 2: Durch Versuch und Irrtum haben wir erfahren, dass der ==Operator den vordefinierten Referenzvergleich verwendet, wenn er einen uneingeschränkten generischen Typ verwendet. Tatsächlich verwendet der Compiler die beste Methode, die er für das Argument des eingeschränkten Typs finden kann, sucht jedoch nicht weiter. Der folgende Code wird beispielsweise immer gedruckt true, auch wenn er Test.test<B>(new B(), new B())aufgerufen wird:

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); } }
Hosam Aly
quelle
Siehe meine Antwort noch einmal für die Antwort auf Ihre Folgefrage.
Giovanni Galbo
Es kann nützlich sein zu verstehen, dass es auch ohne Generika einige Typen gibt, für die das ==zwischen zwei Operanden desselben Typs nicht zulässig ist. Dies gilt für structTypen (außer "vordefinierten" Typen), die das nicht überladen operator ==. Versuchen Sie als einfaches Beispiel Folgendes:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen
Fortsetzung meines eigenen alten Kommentars. Beispiel (siehe anderer Thread ), mit var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, dann können Sie nicht überprüfen, kvp1 == kvp2da KeyValuePair<,>es sich um eine Struktur handelt, es sich nicht um einen vordefinierten C # -Typ handelt und der nicht überladen wird operator ==. Es wird jedoch ein Beispiel gegeben, var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;mit dem Sie nicht arbeiten können e1 == e2(hier haben wir die verschachtelte Struktur List<>.Enumerator( "List`1+Enumerator[T]"von der Laufzeit aufgerufen ), die nicht überladen wird ==).
Jeppe Stig Nielsen
RE: "Warum kann dieses Code-Snippet nicht kompiliert werden?" - Äh ... weil Sie nicht boolvon einem void...
BrainSlugs83
1
@ BrainSlugs83 Danke, dass du einen 10 Jahre alten Fehler entdeckt hast!
Hosam Aly

Antworten:

143

"... verhält sich == standardmäßig wie oben beschrieben für vordefinierte und benutzerdefinierte Referenztypen."

Typ T ist nicht unbedingt ein Referenztyp, daher kann der Compiler diese Annahme nicht treffen.

Dies wird jedoch kompiliert, da es expliziter ist:

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

Folgen Sie der zusätzlichen Frage: "Wenn ich jedoch einen Referenztyp verwende, würde der Operator == den vordefinierten Referenzvergleich verwenden oder würde er die überladene Version des Operators verwenden, wenn ein Typ einen definiert?"

Ich hätte gedacht, dass == auf den Generika die überladene Version verwenden würde, aber der folgende Test zeigt etwas anderes. Interessant ... Ich würde gerne wissen warum! Wenn jemand weiß, bitte teilen.

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);
    }
  }
}

Ausgabe

Inline: Überladen == aufgerufen

Generisch:

Drücken Sie eine beliebige Taste, um fortzufahren . . .

Follow Up 2

Ich möchte darauf hinweisen, dass ich meine Vergleichsmethode auf ändere

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

bewirkt, dass der überladene Operator == aufgerufen wird. Ich denke, ohne Angabe des Typs (als Wo ) kann der Compiler nicht schließen, dass er den überladenen Operator verwenden sollte ... obwohl ich denke, dass er genügend Informationen hätte, um diese Entscheidung zu treffen, auch ohne Angabe des Typs.

Giovanni Galbo
quelle
Vielen Dank. Ich habe nicht bemerkt, dass es sich bei der Aussage nur um Referenztypen handelt.
Hosam Aly
4
Betreff: Follow-up 2: Tatsächlich verknüpft der Compiler die beste Methode, die er findet, in diesem Fall Test.op_Equal. Wenn Sie jedoch eine Klasse hatten, die von Test abgeleitet ist und den Operator überschreibt, wird der Operator von Test weiterhin aufgerufen.
Hosam Aly
4
Ich möchte darauf hinweisen, dass Sie den eigentlichen Vergleich immer innerhalb einer überschriebenen EqualsMethode durchführen sollten (nicht im ==Operator).
Jpbochi
11
Die Überlastungsauflösung erfolgt zur Kompilierungszeit. Wenn wir also ==zwischen generischen Typen Tund haben T, wird die beste Überladung gefunden, wenn man bedenkt, von welchen Einschränkungen sie getragen werden T(es gibt eine spezielle Regel, dass niemals ein Werttyp dafür eingefügt wird (was zu einem bedeutungslosen Ergebnis führen würde), daher muss es eine geben einige Einschränkungen, die garantieren, dass es sich um einen Referenztyp handelt). Wenn Sie in Ihrem Follow-up 2 mit DerivedTestObjekten hereinkommen und DerivedTestvon Testeiner neuen Überladung von stammen ==, diese aber einführen , haben Sie erneut das "Problem". Welche Überladung aufgerufen wird, wird zur Kompilierungszeit in die IL "eingebrannt".
Jeppe Stig Nielsen
1
Seltsamerweise scheint dies für allgemeine Referenztypen zu funktionieren (wo Sie erwarten würden, dass sich dieser Vergleich auf die Referenzgleichheit bezieht), aber für Zeichenfolgen scheint es auch Referenzgleichheit zu verwenden - so können Sie am Ende zwei identische Zeichenfolgen vergleichen und == haben (wenn in a generische Methode mit der Klassenbeschränkung) sagen, dass sie unterschiedlich sind.
JonnyRaa
291

Wie andere gesagt haben, funktioniert es nur, wenn T auf einen Referenztyp beschränkt ist. Ohne Einschränkungen können Sie mit null vergleichen, aber nur mit null - und dieser Vergleich ist für nicht nullfähige Werttypen immer falsch.

Anstatt Equals aufzurufen, ist es besser, ein zu verwenden IComparer<T>- und wenn Sie keine weiteren Informationen haben, EqualityComparer<T>.Defaultist dies eine gute Wahl:

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

Abgesehen von allem anderen vermeidet dies Boxen / Casting.

Jon Skeet
quelle
Vielen Dank. Ich habe versucht, eine einfache Wrapper-Klasse zu schreiben, daher wollte ich die Operation nur an das eigentliche Wrapper-Mitglied delegieren. Aber die Kenntnis von EqualityComparer <T> .Default hat mir sicherlich einen Mehrwert gebracht. :)
Hosam Aly
Nebenbei, Jon; Vielleicht möchten Sie den Kommentar zu pobox vs yoda in meinem Beitrag beachten.
Marc Gravell
4
Netter Tipp zur Verwendung von EqualityComparer <T>
Chakrit
1
+1 für den Hinweis, dass es mit null vergleichen kann und für nicht nullable Werttyp wird es immer falsch sein
Jalal sagte
@BlueRaja: Ja, weil es spezielle Regeln für Vergleiche mit dem Null-Literal gibt. Daher "können Sie ohne Einschränkungen mit null vergleichen, aber nur mit null". Es ist schon in der Antwort. Warum genau kann das nicht richtig sein?
Jon Skeet
41

Im Allgemeinen EqualityComparer<T>.Default.Equalssollte die Arbeit mit allem erledigt werden IEquatable<T>, was implementiert wird oder was eine vernünftige EqualsImplementierung hat.

Wenn jedoch ==und Equalsanders aus irgendwelchen Gründen umgesetzt werden, dann meine Arbeit an generischen Operationen sollte nützlich sein; Es unterstützt unter anderem die Operator- Versionen von:

  • Gleich (T-Wert1, T-Wert2)
  • NotEqual (T-Wert1, T-Wert2)
  • Größer als (T-Wert1, T-Wert2)
  • LessThan (T-Wert1, T-Wert2)
  • GreaterThanOrEqual (T-Wert1, T-Wert2)
  • LessThanOrEqual (T-Wert1, T-Wert2)
Marc Gravell
quelle
Sehr interessante Bibliothek! :) (Randnotiz: Darf ich vorschlagen, den Link zu www.yoda.arachsys.com zu verwenden, da die Pobox an meinem Arbeitsplatz von der Firewall blockiert wurde? Es ist möglich, dass andere das gleiche Problem haben.)
Hosam Aly
Die Idee ist, dass pobox.com/~skeet immer auf meine Website verweist - auch wenn sie an einen anderen Ort verschoben wird . Ich neige dazu, der Nachwelt halber Links über pobox.com zu posten - aber Sie können derzeit stattdessen yoda.arachsys.com ersetzen.
Jon Skeet
Das Problem mit pobox.com ist, dass es sich um einen webbasierten E-Mail-Dienst handelt (so die Firewall des Unternehmens), der blockiert ist. Deshalb konnte ich seinem Link nicht folgen.
Hosam Aly
"Wenn jedoch == und Equals aus irgendeinem Grund unterschiedlich implementiert werden" - Holy Smokes! Was für ein jedoch! Vielleicht muss ich nur einen gegenteiligen Anwendungsfall sehen, aber eine Bibliothek mit divergierender gleicher Semantik wird wahrscheinlich auf größere Probleme stoßen als Probleme mit Generika.
Edward Brey
@ EdwardBrey du liegst nicht falsch; Es wäre schön, wenn der Compiler das durchsetzen könnte, aber ...
Marc Gravell
31

So viele Antworten und keine einzige erklärt das WARUM? (was Giovanni ausdrücklich fragte) ...

.NET-Generika verhalten sich nicht wie C ++ - Vorlagen. In C ++ - Vorlagen tritt eine Überlastungsauflösung auf, nachdem die tatsächlichen Vorlagenparameter bekannt sind.

In .NET-Generika (einschließlich C #) tritt eine Überlastungsauflösung auf, ohne die tatsächlichen generischen Parameter zu kennen. Die einzigen Informationen, die der Compiler zur Auswahl der aufzurufenden Funktion verwenden kann, stammen aus Typbeschränkungen für die generischen Parameter.

Ben Voigt
quelle
2
Aber warum kann der Compiler sie nicht als generisches Objekt behandeln? Immerhin ==funktioniert es für alle Typen, sei es Referenztypen oder Werttypen. Das sollte die Frage sein, auf die Sie meiner Meinung nach nicht geantwortet haben.
Nawfal
4
@nawfal: Eigentlich nein, ==funktioniert nicht für alle Werttypen . Noch wichtiger ist, dass es nicht für alle Typen die gleiche Bedeutung hat, sodass der Compiler nicht weiß, was er damit machen soll.
Ben Voigt
1
Ben, oh ja, ich habe die benutzerdefinierten Strukturen verpasst, die wir ohne erstellen können ==. Können Sie diesen Teil auch in Ihre Antwort aufnehmen, da ich denke, dass dies der Hauptpunkt hier ist
nawfal
12

Die Kompilierung kann nicht wissen, dass T keine Struktur (Werttyp) sein kann. Sie müssen also sagen, dass es nur vom Referenztyp sein kann, denke ich:

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

Dies liegt daran, dass wenn T ein Werttyp sein könnte, es Fälle geben könnte, in denen x == yeine Fehlbildung vorliegt - in Fällen, in denen für einen Typ kein Operator == definiert ist. Dasselbe wird dafür passieren, was offensichtlicher ist:

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

Das schlägt auch fehl, weil Sie einen Typ T übergeben könnten, der keine Funktion foo hätte. C # zwingt Sie sicherzustellen, dass alle möglichen Typen immer eine Funktion foo haben. Dies geschieht durch die where-Klausel.

Johannes Schaub - litb
quelle
1
Danke für die Klarstellung. Ich wusste nicht, dass Werttypen den Operator == nicht sofort unterstützen.
Hosam Aly
1
Hosam, ich habe mit gmcs (mono) getestet und es vergleicht immer Referenzen. (dh es wird kein optional definierter Operator == für T verwendet)
Johannes Schaub - litb
Bei dieser Lösung gibt es eine Einschränkung: Der Operator == kann nicht überladen werden. Siehe diese StackOverflow-Frage .
Dimitri C.
8

Es scheint, dass ohne die Klassenbeschränkung:

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

Man sollte sich darüber im Klaren sein, dass während eine classEinschränkung Equalsim ==Operator von erbt Object.Equals, während die einer Struktur überschreibt ValueType.Equals.

Beachten Sie, dass:

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

gibt auch den gleichen Compilerfehler aus.

Bis jetzt verstehe ich nicht, warum ein Vergleich des Werttyp-Gleichheitsoperators vom Compiler abgelehnt wird. Ich weiß jedoch, dass dies funktioniert:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Jon Limjap
quelle
Du weißt, ich bin ein totaler C # Noob. aber ich denke, es schlägt fehl, weil der Compiler nicht weiß, was zu tun ist. Da T noch nicht bekannt ist, hängt es vom Typ T ab, ob Werttypen zulässig sind. Bei Referenzen werden die Referenzen nur unabhängig von T. verglichen. Wenn Sie .Equals ausführen, wird .Equal nur aufgerufen.
Johannes Schaub - litb
Wenn Sie jedoch == für einen Wertetyp ausführen, muss der Werttyp diesen Operator nicht unbedingt implementieren.
Johannes Schaub - litb
Das würde Sinn machen, litb :) Es ist möglich, dass benutzerdefinierte Strukturen == nicht überladen, daher schlägt der Compiler fehl.
Jon Limjap
2
Der erste Vergleich Methode funktioniert nicht verwenden , Object.Equalssondern prüft Referenz Gleichheit. Zum Beispiel Compare("0", 0.ToString())würde false zurückgegeben, da die Argumente Verweise auf unterschiedliche Zeichenfolgen sind, die beide eine Null als einziges Zeichen haben.
Superkatze
1
Kleinere Probleme mit dem letzten - Sie haben es nicht auf Strukturen beschränkt, also NullReferenceExceptionkönnte ein Ereignis auftreten .
Flynn1179
6

Nun, in meinem Fall wollte ich den Gleichheitsoperator einem Unit-Test unterziehen. Ich musste den Code unter den Gleichheitsoperatoren aufrufen, ohne den generischen Typ explizit festzulegen. Ratschläge für EqualityComparerwaren als EqualityCompareraufgerufene EqualsMethode nicht hilfreich , aber nicht als Gleichheitsoperator.

Hier ist, wie ich das mit generischen Typen arbeiten lasse, indem ich ein baue LINQ. Es ruft den richtigen Code für ==und !=Operatoren auf:

/// <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);
}
U. Bulle
quelle
4

Es gibt eine MSDN Connect - Eintrag für diese hier

Die Antwort von Alex Turner beginnt mit:

Leider ist dieses Verhalten beabsichtigt und es gibt keine einfache Lösung, um die Verwendung von == mit Typparametern zu ermöglichen, die Werttypen enthalten können.

Recep
quelle
4

Wenn Sie sicherstellen möchten, dass die Operatoren Ihres benutzerdefinierten Typs aufgerufen werden, können Sie dies über Reflection tun. Rufen Sie einfach den Typ mit Ihrem generischen Parameter ab und rufen Sie die MethodInfo für den gewünschten Operator ab (z. B. op_Equality, op_Inequality, op_LessThan ...).

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

Führen Sie dann den Operator mit der Invoke-Methode von MethodInfo aus und übergeben Sie die Objekte als Parameter.

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

Dadurch wird Ihr überladener Operator aufgerufen und nicht derjenige, der durch die auf den generischen Parameter angewendeten Einschränkungen definiert ist. Dies ist möglicherweise nicht praktikabel, kann sich jedoch als nützlich für Unit-Tests Ihrer Bediener erweisen, wenn Sie eine generische Basisklasse verwenden, die einige Tests enthält.

Christophe
quelle
3

Ich habe die folgende Funktion geschrieben und mir die neueste msdn angesehen. Es kann leicht zwei Objekte vergleichen xund y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Charlie
quelle
4
Sie können Ihre Booleschen return ((IComparable)(x)).CompareTo(y) <= 0;
Werte
1

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

Dies funktioniert, da == bei benutzerdefinierten Referenztypen berücksichtigt wird.
Bei Werttypen kann == überschrieben werden. In diesem Fall sollte auch "! =" Definiert werden.

Ich denke, das könnte der Grund sein, es erlaubt keinen generischen Vergleich mit "==".

shahkalpesh
quelle
2
Vielen Dank. Ich glaube, Referenztypen können auch den Operator überschreiben. Aber der Fehlergrund ist jetzt klar.
Hosam Aly
1
Das ==Token wird für zwei verschiedene Operatoren verwendet. Wenn für die angegebenen Operandentypen eine kompatible Überladung des Gleichheitsoperators vorliegt, wird diese Überladung verwendet. Andernfalls wird ein Referenzvergleich verwendet, wenn beide Operanden Referenztypen sind, die miteinander kompatibel sind. Beachten Sie, dass Compareder Compiler in der obigen Methode nicht erkennen kann, dass die erste Bedeutung gilt, aber die zweite Bedeutung zutreffen kann, sodass das ==Token diese auch dann verwendet, wenn Tder Operator für die Gleichheitsprüfung überlastet wird (z. B. wenn es sich um einen Typ handelt String) .
Supercat
0

Das .Equals()funktioniert bei mir zwar TKeyein generischer Typ.

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


    // ...
}
Masoud Darvishian
quelle
Das ist x.Id.Equalsnicht so id.Equals. Vermutlich weiß der Compiler etwas über den Typ von x.
Hosam Aly