Null- oder Standardvergleich des generischen Arguments in C #

288

Ich habe eine generische Methode wie folgt definiert:

public void MyMethod<T>(T myArgument)

Als erstes möchte ich überprüfen, ob der Wert von myArgument der Standardwert für diesen Typ ist.

if (myArgument == default(T))

Dies wird jedoch nicht kompiliert, da ich nicht garantiert habe, dass T den Operator == implementiert. Also habe ich den Code auf Folgendes umgestellt:

if (myArgument.Equals(default(T)))

Jetzt wird dies kompiliert, schlägt jedoch fehl, wenn myArgument null ist. Dies ist Teil dessen, worauf ich teste. Ich kann eine explizite Nullprüfung wie folgt hinzufügen:

if (myArgument == null || myArgument.Equals(default(T)))

Das fühlt sich für mich überflüssig an. ReSharper schlägt sogar vor, den Teil myArgument == null in myArgument == default (T) zu ändern, mit dem ich begonnen habe. Gibt es einen besseren Weg, um dieses Problem zu lösen?

Ich muss sowohl Referenztypen als auch Werttypen unterstützen.

Stefan Moser
quelle
C # unterstützt jetzt Null Conditional Operators , was für das letzte Beispiel, das Sie geben, syntatischer Zucker ist. Ihr Code würde werden if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU
1
@ wizard07KSU Das funktioniert nicht für Werttypen, dh wird auf truejeden Fall ausgewertet, da Equalsimmer für Werttypen aufgerufen wird, da myArgumentdies nullin diesem Fall nicht möglich ist und das Ergebnis vonEquals (einem Booleschen ) niemals sein wird null.
Jasper
Gleichermaßen wertvolles Fast-Duplikat (also nicht zum Schließen abstimmen): Kann der Operator == nicht auf generische Typen in C # angewendet werden?
GSerg

Antworten:

582

Um Boxen zu vermeiden, ist der beste Weg, Generika auf Gleichheit zu vergleichen, mit EqualityComparer<T>.Default. Dies respektiert IEquatable<T>(ohne Boxen) sowie object.Equalsalle Nullable<T>"angehobenen" Nuancen und behandelt sie . Daher:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

Dies wird übereinstimmen:

  • null für Klassen
  • null (leer) für Nullable<T>
  • Null / False / etc für andere Strukturen
Marc Gravell
quelle
28
Wow, wie herrlich dunkel! Dies ist definitiv der richtige Weg, ein dickes Lob.
Nick Farina
1
Auf jeden Fall die beste Antwort. Keine verschnörkelten Zeilen in meinem Code nach dem Umschreiben, um diese Lösung zu verwenden.
Nathan Ridley
13
Gute Antwort! Noch besser ist es, eine Erweiterungsmethode für diese Codezeile hinzuzufügen, damit Sie obj.IsDefaultForType ()
rikoe
2
@nawfal im Fall Person, p1.Equals(p2)würde davon abhängen , ob es Geräte IEquatable<Person>auf der öffentlichen API oder über explizite Implementierung - also eine öffentliche die Compiler sehen Equals(Person other)Methode. Jedoch; Bei Generika wird für alle dieselbe IL verwendet T. ein , T1die zur Umsetzung geschieht IEquatable<T1>Bedürfnisse behandelt wird identisch zu einem T2, der nicht - also nein, es wird nicht ein Fleck Equals(T1 other)Verfahren, auch wenn es zur Laufzeit vorhanden ist . In beiden Fällen gibt es auch nullzu überlegen (jedes Objekt). Bei Generika würde ich also den Code verwenden, den ich gepostet habe.
Marc Gravell
5
Ich kann mich nicht entscheiden, ob diese Antwort mich vom Wahnsinn weg oder näher gebracht hat. +1
Steven Liekens
118

Wie wäre es damit:

if (object.Equals(myArgument, default(T)))
{
    //...
}

Durch die Verwendung der static object.Equals()Methode müssen Sie die nullÜberprüfung nicht selbst durchführen. object.Abhängig von Ihrem Kontext ist es wahrscheinlich nicht erforderlich, den Anruf explizit zu qualifizieren , aber normalerweise staticstelle ich Anrufen den Typnamen voran, um den Code löslicher zu machen.

Kent Boogaart
quelle
2
Sie können das "Objekt" sogar löschen. Teil, da es überflüssig ist. if (Equals (myArgument, default (T)))
Stefan Moser
13
Dies ist normalerweise der Fall, hängt jedoch möglicherweise nicht vom Kontext ab. Möglicherweise gibt es eine Instanz-Equals () -Methode, die zwei Argumente akzeptiert. Ich neige dazu, allen statischen Aufrufen explizit den Klassennamen voranzustellen, um den Code leichter lesbar zu machen.
Kent Boogaart
8
Beachten Sie, dass es Boxen verursachen wird und in einigen Fällen wichtig sein kann
Nightcoder
2
Für mich funktioniert dies nicht, wenn Sie Ganzzahlen verwenden, die bereits eingerahmt sind. Weil es dann ein Objekt sein wird und der Standard für Objekt null statt 0 ist.
riezebosch
28

Ich konnte einen Microsoft Connect-Artikel finden , in dem dieses Problem ausführlich behandelt wird:

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

Wenn bekannt ist, dass es sich bei den Typen um Referenztypen handelt, testet die Standardüberladung der auf Objekt definierten Variablen die Referenzgleichheit, obwohl ein Typ möglicherweise eine eigene benutzerdefinierte Überladung angibt. Der Compiler bestimmt anhand des statischen Typs der Variablen, welche Überladung verwendet werden soll (die Bestimmung ist nicht polymorph). Wenn Sie Ihr Beispiel ändern, um den generischen Typparameter T auf einen nicht versiegelten Referenztyp (z. B. Ausnahme) zu beschränken, kann der Compiler daher die zu verwendende spezifische Überladung ermitteln und den folgenden Code kompilieren:

public class Test<T> where T : Exception

Wenn bekannt ist, dass es sich bei den Typen um Werttypen handelt, werden basierend auf den genauen verwendeten Typen spezifische Wertgleichheitstests durchgeführt. Hier gibt es keinen guten "Standard" -Vergleich, da Referenzvergleiche für Werttypen nicht aussagekräftig sind und der Compiler nicht wissen kann, welcher spezifische Wertvergleich ausgegeben werden soll. Der Compiler könnte einen Aufruf von ValueType.Equals (Object) ausgeben, aber diese Methode verwendet Reflektion und ist im Vergleich zu den spezifischen Wertvergleichen ziemlich ineffizient. Selbst wenn Sie eine Werttypbeschränkung für T angeben, kann der Compiler hier daher nichts generieren:

public class Test<T> where T : struct

In dem von Ihnen vorgestellten Fall, in dem der Compiler nicht einmal weiß, ob T ein Wert oder ein Referenztyp ist, muss ebenfalls nichts generiert werden, was für alle möglichen Typen gültig wäre. Ein Referenzvergleich wäre für Werttypen nicht gültig, und ein Wertvergleich wäre für Referenztypen, die nicht überladen sind, unerwartet.

Folgendes können Sie tun ...

Ich habe bestätigt, dass beide Methoden für einen generischen Vergleich von Referenz- und Werttypen funktionieren:

object.Equals(param, default(T))

oder

EqualityComparer<T>.Default.Equals(param, default(T))

Um Vergleiche mit dem Operator "==" durchzuführen, müssen Sie eine der folgenden Methoden verwenden:

Wenn alle Fälle von T von einer bekannten Basisklasse stammen, können Sie den Compiler mithilfe generischer Typbeschränkungen informieren.

public void MyMethod<T>(T myArgument) where T : MyBase

Der Compiler erkennt dann, wie Operationen ausgeführt werden MyBase und gibt nicht aus, dass der Operator "==" nicht auf Operanden vom Typ "T" und "T" angewendet werden kann, die jetzt angezeigt werden.

Eine andere Möglichkeit wäre, T auf jeden Typ zu beschränken, der implementiert IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

Verwenden Sie dann die CompareTovon der IComparable-Schnittstelle definierte Methode .

Eric Schoonover
quelle
4
"Dieses Verhalten ist beabsichtigt und es gibt keine einfache Lösung, um die Verwendung von Typparametern zu ermöglichen, die möglicherweise Werttypen enthalten." Eigentlich ist Microsoft falsch. Es gibt eine einfache Lösung: MS sollte den ceq-Opcode so erweitern, dass er für Werttypen als bitweiser Operator fungiert. Dann könnten sie einen Intrinsic bereitstellen, der einfach diesen Opcode verwendet, z. B. object.BitwiseOrReferenceEquals <T> (Wert, Standard (T)), der einfach ceq verwendet. Sowohl für Wert- als auch für Referenztypen würde dies die bitweise Gleichheit des Werts überprüfen (aber für Referenztypen ist die bitweise Referenzgleichheit dieselbe wie für object.ReferenceEquals)
Qwertie
1
Ich denke, der Microsoft Connect-Link, den Sie wollten, war connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie
18

Versuche dies:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

das sollte kompilieren und tun, was Sie wollen.

Lasse V. Karlsen
quelle
Ist die <code> Standardeinstellung (T) </ code> nicht redundant? <code> EqualityComparer <T> .Default.Equals (myArgument) </ code> sollte den Trick machen.
Joshcodes
2
1) hast du es versucht und 2) womit vergleichst du dann das Vergleichsobjekt? Die EqualsMethode verwendet IEqualityComparerzwei Argumente, die beiden zu vergleichenden Objekte. Nein, sie ist nicht redundant.
Lasse V. Karlsen
Dies ist sogar besser als die akzeptierte Antwort IMHO, da es Boxen / Unboxing und andere Arten behandelt. Siehe die Antwort der Fragen "Closed as Dupe": stackoverflow.com/a/864860/210780
ashes999
7

(Bearbeitet)

Marc Gravell hat die beste Antwort, aber ich wollte ein einfaches Code-Snippet veröffentlichen, das ich ausgearbeitet habe, um es zu demonstrieren. Führen Sie dies einfach in einer einfachen C # -Konsolen-App aus:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

Noch etwas: Kann jemand mit VS2008 dies als Erweiterungsmethode versuchen? Ich stecke hier mit 2005 fest und bin gespannt, ob das erlaubt ist.


Bearbeiten: Hier erfahren Sie, wie Sie es als Erweiterungsmethode verwenden können:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}
Joel Coehoorn
quelle
3
Es "funktioniert" als Erweiterungsmethode. Das ist interessant, da es auch funktioniert, wenn Sie o.IsDefault <Objekt> () sagen, wenn o null ist. Scary =)
Nick Farina
6

Um alle Arten von T zu behandeln, einschließlich der Tatsache, dass T ein primitiver Typ ist, müssen Sie beide Vergleichsmethoden kompilieren:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }
Nick Farina
quelle
1
Beachten Sie, dass die Funktion geändert wurde, um Func <T> zu akzeptieren und T zurückzugeben, was meiner Meinung nach versehentlich im Code des Fragestellers weggelassen wurde.
Nick Farina
Scheint, als würde ReSharper mich durcheinander bringen. Wusste nicht, dass die Warnung vor einem möglichen Vergleich zwischen einem Werttyp und null keine Compiler-Warnung war.
Nathan Ridley
2
Zu Ihrer Information: Wenn sich herausstellt, dass T ein Werttyp ist, wird der Vergleich mit Null vom Jitter als immer falsch behandelt.
Eric Lippert
Sinnvoll - Die Laufzeit vergleicht einen Zeiger mit einem Werttyp. Die Equals () - Prüfung funktioniert in diesem Fall jedoch (interessanterweise, da es sehr dynamisch erscheint, 5.Equals (4) zu sagen, das kompiliert wird).
Nick Farina
2
In der Antwort von EqualityComparer <T> finden Sie eine Alternative, die kein Boxen beinhaltet
Marc Gravell
2

Hier wird es ein Problem geben -

Wenn Sie zulassen, dass dies für jeden Typ funktioniert, ist Standard (T) für Referenztypen immer null und für Werttypen 0 (oder eine Struktur voller 0).

Dies ist jedoch wahrscheinlich nicht das Verhalten, nach dem Sie suchen. Wenn dies generisch funktionieren soll, müssen Sie wahrscheinlich Reflection verwenden, um den Typ von T zu überprüfen und andere Werttypen als Referenztypen zu behandeln.

Alternativ können Sie eine Schnittstellenbeschränkung festlegen, und die Schnittstelle bietet eine Möglichkeit, den Standard der Klasse / Struktur zu überprüfen.

Reed Copsey
quelle
1

Ich denke, Sie müssen diese Logik wahrscheinlich in zwei Teile aufteilen und zuerst nach Null suchen.

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

Bei der IsNull-Methode verlassen wir uns auf die Tatsache, dass ValueType-Objekte per Definition nicht null sein können. Wenn value also eine Klasse ist, die von ValueType abgeleitet ist, wissen wir bereits, dass sie nicht null ist. Wenn es sich jedoch nicht um einen Wertetyp handelt, können wir den Wert, der mit einem Objekt umgewandelt wurde, einfach mit Null vergleichen. Wir könnten die Prüfung gegen ValueType vermeiden, indem wir direkt zu einem Cast-to-Objekt wechseln. Dies würde jedoch bedeuten, dass ein Werttyp eingerahmt wird, was wir wahrscheinlich vermeiden möchten, da dies impliziert, dass ein neues Objekt auf dem Heap erstellt wird.

In der IsNullOrEmpty-Methode suchen wir nach dem Sonderfall einer Zeichenfolge. Für alle anderen Typen vergleichen wir den Wert (der bereits weiß, dass dies nicht der Fall ist) null ist) mit seinem Standardwert, der für alle Referenztypen null ist und für Werttypen normalerweise eine Form von Null ist (wenn sie ganzzahlig sind).

Mit diesen Methoden verhält sich der folgende Code wie erwartet:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}
Damian Powell
quelle
1

Verlängerungsmethode basierend auf akzeptierter Antwort.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Verwendungszweck:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Wechseln Sie zur Vereinfachung mit null ab:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Verwendungszweck:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
dynamiclynk
quelle
0

Ich benutze:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}
Kofifus
quelle
-1

Sie wissen nicht, ob dies mit Ihren Anforderungen funktioniert oder nicht, aber Sie können T auf einen Typ beschränken, der eine Schnittstelle wie IComparable implementiert, und dann die ComparesTo () -Methode von dieser Schnittstelle (die von IIRC Nullen unterstützt / behandelt) verwenden ::

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

Es gibt wahrscheinlich andere Schnittstellen, die Sie auch IEquitable usw. verwenden könnten.

Caryden
quelle
OP ist besorgt über NullReferenceException und Sie garantieren ihm dasselbe.
Nawfal
-2

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

Der Operator '==' kann nicht auf Operanden vom Typ 'T' und 'T' angewendet werden.

Ich kann mir keine Möglichkeit vorstellen, dies ohne den expliziten Nulltest zu tun, gefolgt vom Aufrufen der Equals-Methode oder des Equals-Objekts. Equals wie oben vorgeschlagen.

Sie können mit System.Comparison eine Lösung entwickeln, aber das wird tatsächlich zu viel mehr Codezeilen führen und die Komplexität erheblich erhöhen.

cfeduke
quelle
-3

Ich denke du warst nah dran.

if (myArgument.Equals(default(T)))

Jetzt kompiliert dies, wird aber fehlschlagen, wenn myArgument es null ist. ist ein Teil dessen, worauf ich teste. Ich kann eine explizite Nullprüfung wie folgt hinzufügen:

Sie müssen nur das Objekt umkehren, für das die Gleichheit aufgerufen wird, um einen eleganten, nullsicheren Ansatz zu erhalten.

default(T).Equals(myArgument);
Scott McKay
quelle
Ich dachte genau das Gleiche.
Chris Gessler
6
Standard (T) eines Referenztyps ist null und führt zu einer garantierten NullReferenceException.
Stefan Steinegger