it-swarm-eu.dev

Null- oder Standardvergleich eines generischen Arguments in C #

Ich habe eine generische Methode wie folgt definiert:

public void MyMethod<T>(T myArgument)

Als Erstes möchte ich prüfen, ob der Wert von myArgument der Standardwert für diesen Typ ist, etwa wie folgt:

if (myArgument == default(T))

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

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

Nun wird dies kompiliert, aber es schlägt fehl, wenn meinArgument den Wert null hat. Ich kann eine explizite Nullprüfung wie folgt hinzufügen:

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

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

Ich muss unterstützen beide Verweistypen und Werttypen.

239
Stefan Moser

Um Boxen zu vermeiden, ist der beste Weg zum Vergleich von Generics für Gleichheit mit EqualityComparer<T>.Default. Dies gilt sowohl für IEquatable<T> (ohne Boxen) als auch für object.Equals und behandelt alle Nullable<T> "angehobenen" Nuancen. Daher:

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

Das wird passen:

  • null für Klassen
  • null (leer) für Nullable<T>
  • null/falsch/etc für andere Strukturen
486
Marc Gravell

Wie wäre es damit:

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

Durch die Verwendung der static object.Equals()-Methode müssen Sie die null-Prüfung nicht selbst durchführen. Das explizite Qualifizieren des Aufrufs mit object. ist wahrscheinlich abhängig von Ihrem Kontext nicht erforderlich, aber normalerweise static-Aufrufen werde der Typname vorangestellt, um den Code löslicher zu machen.

113
Kent Boogaart

Ich konnte einen Microsoft Connect-Artikel finden, in dem dieses Problem detailliert beschrieben 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, prüft die Standardüberladung von für Objekt definierten Variablen die Referenzgleichheit, obwohl ein Typ seine eigene benutzerdefinierte Überladung angeben kann. Der Compiler bestimmt anhand des statischen Typs der Variablen, welche Überladung verwendet werden soll (die Bestimmung ist nicht polymorph). Wenn Sie also Ihr Beispiel ändern, um den generischen Typparameter T auf einen nicht gesiegelten Referenztyp (z. B. Exception) zu beschränken, kann der Compiler die zu verwendende Überladung bestimmen 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 auf der Grundlage der genauen verwendeten Typen spezifische Wertgleichheitstests durchgeführt. Es gibt hier keinen guten "Default" -Vergleich, da Referenzvergleiche für Werttypen keine Bedeutung haben und der Compiler nicht wissen kann, welchen spezifischen Wertvergleich ausgeben soll. Der Compiler könnte einen Aufruf von ValueType.Equals (Object) ausgeben, aber diese Methode verwendet Reflektion und ist im Vergleich zu den spezifischen Wertevergleichen ziemlich ineffizient. Selbst wenn Sie eine Werttypeinschränkung für T angeben, ist es daher für den Compiler nicht sinnvoll, hier zu 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, gibt es ebenfalls nichts zu generieren, das für alle möglichen Typen gültig wäre. Ein Referenzvergleich wäre für Werttypen nicht gültig und eine Art Wertvergleich wäre für Referenztypen, die nicht überladen werden, unerwartet.

Hier können Sie tun ...

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

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 über generische Typeinschränkungen informieren.

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

Der Compiler erkennt dann, wie Operationen an MyBase ausgeführt werden sollen, und gibt den Fehler "Operator '==' kann nicht auf Operanden vom Typ 'T' und 'T' 'angewendet werden, die jetzt angezeigt werden.

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

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

Verwenden Sie dann die von der IComparable-Schnittstelle definierte CompareTo-Methode.

24
Eric Schoonover

Versuche dies:

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

das sollte kompilieren und tun, was Sie wollen.

(Bearbeitet)

Marc Gravell hat die beste Antwort, aber ich wollte ein einfaches Code-Snippet posten, das ich zur Demonstration aufgearbeitet habe. 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 ausprobieren? Ich bleibe bei 2005 dabei und bin gespannt, ob das erlaubt wäre.


Edit: So funktioniert es als Erweiterungsmethode:

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

Um mit allen T-Typen umgehen zu können, auch wenn 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;
    }
6
Nick Farina

Es wird hier ein Problem geben -

Wenn Sie zulassen möchten, dass dies für einen beliebigen Typ funktioniert, ist default (T) für Referenztypen immer null und für Werttypen 0 (oder eine Struktur mit 0).

Dies ist wahrscheinlich nicht das Verhalten, nach dem Sie suchen. Wenn Sie möchten, dass dies auf eine allgemeine Weise funktioniert, müssen Sie möglicherweise mit Reflection den Typ von T überprüfen und andere Wertetypen als Referenztypen verwenden.

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

2
Reed Copsey

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

In der IsNull-Methode setzen wir darauf, dass ValueType-Objekte per Definition nicht null sein können. Wenn value eine Klasse ist, die von ValueType abgeleitet ist, wissen wir bereits, dass sie nicht null ist. Wenn es sich jedoch nicht um einen Werttyp handelt, können wir einfach die Umwandlung von Werten mit einem Objekt gegen Null vergleichen. Wir könnten die Prüfung gegen ValueType vermeiden, indem wir direkt zu einem Cast-to-Objekt wechseln, aber das würde bedeuten, dass ein Werttyp in einen Box-Zustand versetzt würde.

In der IsNullOrEmpty-Methode überprüfen wir den Sonderfall einer Zeichenfolge. Bei allen anderen Typen vergleichen wir den Wert (der bereits bekannt ist nicht null) mit seinem Standardwert, der für alle Referenztypen Null ist und für Wertetypen normalerweise eine Form von Null ist (wenn sie integral 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) ...
}
1
Damian Powell

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;
  }
}
0
kofifus