C # generische Typeinschränkung für alles nullbar


110

Also habe ich diese Klasse:

public class Foo<T> where T : ???
{
    private T item;

    public bool IsNull()
    {
        return item == null;
    }

}

Jetzt suche ich nach einer Typeinschränkung, mit der ich alles als Typparameter verwenden kann null. Das bedeutet alle Referenztypen sowie alle Nullable( T?) Typen:

Foo<String> ... = ...
Foo<int?> ... = ...

sollte möglich sein.

Wenn classich als Typeinschränkung verwende, kann ich nur die Referenztypen verwenden.

Zusätzliche Informationen: Ich schreibe eine Pipes and Filter-Anwendung und möchte eine nullReferenz als letztes Element verwenden, das in die Pipeline übergeht, damit jeder Filter ordnungsgemäß heruntergefahren, bereinigt usw. werden kann.


1
@ Tim, der keine Nullables erlaubt
Rik


2
Dies ist nicht direkt möglich. Vielleicht können Sie uns mehr über Ihr Szenario erzählen? Oder könnten Sie IFoo<T>als Arbeitstyp verwenden und Instanzen über eine Factory-Methode erstellen? Das könnte funktionieren.
Jon

Ich bin mir nicht sicher, warum Sie etwas auf diese Weise einschränken möchten oder müssen. Wenn Ihre einzige Absicht darin besteht, "if x == null" in if x.IsNull () umzuwandeln, "erscheint dies für 99,99% der Entwickler, die an die frühere Syntax gewöhnt sind, sinnlos und nicht intuitiv. Der Compiler lässt Sie dies nicht zu." if (int) x == null "sowieso, so sind Sie bereits abgedeckt.
RJ Lohan

Antworten:


22

Wenn Sie bereit sind, eine Laufzeitprüfung im Konstruktor von Foo durchzuführen, anstatt eine Prüfung zur Kompilierungszeit durchzuführen, können Sie prüfen, ob der Typ keine Referenz oder kein nullbarer Typ ist, und in diesem Fall eine Ausnahme auslösen.

Mir ist klar, dass nur eine Laufzeitprüfung inakzeptabel sein kann, aber nur für den Fall:

public class Foo<T>
{
    private T item;

    public Foo()
    {
        var type = typeof(T);

        if (Nullable.GetUnderlyingType(type) != null)
            return;

        if (type.IsClass)
            return;

        throw new InvalidOperationException("Type is not nullable or reference type.");
    }

    public bool IsNull()
    {
        return item == null;
    }
}

Dann wird der folgende Code kompiliert, aber der letzte ( foo3) löst eine Ausnahme im Konstruktor aus:

var foo1 = new Foo<int?>();
Console.WriteLine(foo1.IsNull());

var foo2 = new Foo<string>();
Console.WriteLine(foo2.IsNull());

var foo3= new Foo<int>();  // THROWS
Console.WriteLine(foo3.IsNull());

31
Wenn Sie dies tun, stellen Sie sicher, dass Sie die Überprüfung im statischen Konstruktor durchführen, sonst verlangsamen Sie die Konstruktion jeder Instanz Ihrer generischen Klasse (unnötig)
Eamon Nerbonne

2
@EamonNerbonne Sie sollten keine Ausnahmen von statischen Konstruktoren auslösen
Matthew Watson

5
Richtlinien sind nicht absolut. Wenn Sie diese Prüfung wünschen, müssen Sie die Kosten einer Laufzeitprüfung gegen die Unhandlichkeit von Ausnahmen in einem statischen Konstruktor abwägen. Da Sie hier wirklich einen statischen Analysator für arme Männer implementieren, sollte diese Ausnahme nur während der Entwicklung ausgelöst werden. Selbst wenn Sie statische Konstruktionsausnahmen um jeden Preis vermeiden möchten (unklug), sollten Sie im Instanzkonstruktor so viel Arbeit wie möglich statisch und so wenig wie möglich ausführen - z. B. indem Sie ein Flag "isBorked" oder was auch immer setzen.
Eamon Nerbonne

Ich denke übrigens nicht, dass Sie versuchen sollten, dies überhaupt zu tun. In den meisten Fällen würde ich es vorziehen, dies nur als C # -Einschränkung zu akzeptieren, anstatt zu versuchen, mit einer undichten, fehleranfälligen Abstraktion zu arbeiten. Eine andere Lösung könnte beispielsweise darin bestehen, nur Klassen oder nur Strukturen zu benötigen (und sie explizit auf null zu setzen) - oder beides zu tun und zwei Versionen zu haben. Das ist keine Kritik an dieser Lösung; Es ist nur so, dass dieses Problem nicht gut gelöst werden kann - es sei denn, Sie sind bereit, einen benutzerdefinierten Roslyn-Analysator zu schreiben.
Eamon Nerbonne

1
Sie können das Beste aus beiden Welten herausholen: Behalten Sie ein static bool isValidTypeFeld bei, das Sie im statischen Konstruktor festgelegt haben, überprüfen Sie dann einfach dieses Flag im Instanzkonstruktor und werfen Sie, wenn es sich um einen ungültigen Typ handelt, damit Sie nicht jedes Mal die gesamte Überprüfungsarbeit ausführen eine Instanz. Ich benutze dieses Muster oft.
Mike Marynowski

20

Ich weiß nicht, wie man ein Äquivalent zu OR in Generika implementiert . Ich kann jedoch vorschlagen, ein Standardschlüsselwort zu verwenden , um null für nullfähige Typen und einen Wert von 0 für Strukturen zu erstellen:

public class Foo<T>
{
    private T item;

    public bool IsNullOrDefault()
    {
        return Equals(item, default(T));
    }
}

Sie können auch Ihre Version von Nullable implementieren:

class MyNullable<T> where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(MyNullable<T> value)
    {
        return value != null ? value.Value : default(T);
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T> { Value = value };
    }
}

class Foo<T> where T : class
{
    public T Item { get; set; }

    public bool IsNull()
    {
        return Item == null;
    }
}

Beispiel:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Foo<MyNullable<int>>().IsNull()); // true
        Console.WriteLine(new Foo<MyNullable<int>> {Item = 3}.IsNull()); // false
        Console.WriteLine(new Foo<object>().IsNull()); // true
        Console.WriteLine(new Foo<object> {Item = new object()}.IsNull()); // false

        var foo5 = new Foo<MyNullable<int>>();
        int integer = foo5.Item;
        Console.WriteLine(integer); // 0

        var foo6 = new Foo<MyNullable<double>>();
        double real = foo6.Item;
        Console.WriteLine(real); // 0

        var foo7 = new Foo<MyNullable<double>>();
        foo7.Item = null;
        Console.WriteLine(foo7.Item); // 0
        Console.WriteLine(foo7.IsNull()); // true
        foo7.Item = 3.5;
        Console.WriteLine(foo7.Item); // 3.5
        Console.WriteLine(foo7.IsNull()); // false

        // var foo5 = new Foo<int>(); // Not compile
    }
}

Das ursprüngliche Nullable <T> im Framework ist eine Struktur, keine Klasse. Ich denke nicht, dass es eine gute Idee ist, einen Referenztyp-Wrapper zu erstellen, der einen Werttyp nachahmt.
Niall Connaughton

1
Der erste Vorschlag mit Standard ist perfekt! Jetzt kann meine Vorlage mit einem generischen Typ, der zurückgegeben wird, eine Null für Objekte und den Standardwert für integrierte Typen zurückgeben.
Casey Anderson

13

Ich bin auf dieses Problem gestoßen, weil es einfacher war, eine generische statische Methode zu wollen, die alles "Nullable" (entweder Referenztypen oder Nullables) annehmen kann, was mich zu dieser Frage ohne zufriedenstellende Lösung brachte. Also habe ich meine eigene Lösung gefunden, die relativ einfacher zu lösen war als die vom OP angegebene Frage, indem ich einfach zwei überladene Methoden hatte, eine, die a Tund die Einschränkung hat, where T : classund eine, die a T?und hat where T : struct.

Dann wurde mir klar, dass diese Lösung auch auf dieses Problem angewendet werden kann, um eine Lösung zu erstellen, die beim Kompilieren überprüft werden kann, indem der Konstruktor privat (oder geschützt) gemacht und eine statische Factory-Methode verwendet wird:

    //this class is to avoid having to supply generic type arguments 
    //to the static factory call (see CA1000)
    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return Foo<TFoo>.Create(value);
        }

        public static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return Foo<TFoo?>.Create(value);
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo(T value)
        {
            item = value;
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return new Foo<TFoo>(value);
        }

        internal static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return new Foo<TFoo?>(value);
        }
    }

Jetzt können wir es so verwenden:

        var foo1 = new Foo<int>(1); //does not compile
        var foo2 = Foo.Create(2); //does not compile
        var foo3 = Foo.Create(""); //compiles
        var foo4 = Foo.Create(new object()); //compiles
        var foo5 = Foo.Create((int?)5); //compiles

Wenn Sie einen parameterlosen Konstruktor wollen, werden Sie nicht die Überladung bekommen, aber Sie können trotzdem so etwas tun:

    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return Foo<TFoo>.Create<TFoo>();
        }

        public static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return Foo<TFoo?>.CreateNullable<TFoo>();
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo()
        {
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return new Foo<TFoo>();
        }

        internal static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return new Foo<TFoo?>();
        }
    }

Und benutze es so:

        var foo1 = new Foo<int>(); //does not compile
        var foo2 = Foo.Create<int>(); //does not compile
        var foo3 = Foo.Create<string>(); //compiles
        var foo4 = Foo.Create<object>(); //compiles
        var foo5 = Foo.CreateNullable<int>(); //compiles

Diese Lösung hat nur wenige Nachteile. Zum einen bevorzugen Sie möglicherweise die Verwendung von 'new' zum Erstellen von Objekten. Ein weiterer Grund ist, dass Sie nicht Foo<T>als generisches Typargument für eine Typeinschränkung verwenden können, die Folgendes umfasst : where TFoo: new(). Schließlich ist hier der zusätzliche Code, den Sie benötigen, insbesondere wenn Sie mehrere überladene Konstruktoren benötigen.


8

Wie bereits erwähnt, können Sie keine Überprüfung der Kompilierungszeit durchführen lassen. Allgemeine Einschränkungen in .NET fehlen stark und unterstützen die meisten Szenarien nicht.

Ich halte dies jedoch für eine bessere Lösung für die Laufzeitprüfung. Es kann zur JIT-Kompilierungszeit optimiert werden, da beide Konstanten sind.

public class SomeClass<T>
{
    public SomeClass()
    {
        // JIT-compile time check, so it doesn't even have to evaluate.
        if (default(T) != null)
            throw new InvalidOperationException("SomeClass<T> requires T to be a nullable type.");

        T variable;
        // This still won't compile
        // variable = null;
        // but because you know it's a nullable type, this works just fine
        variable = default(T);
    }
}

3

Eine solche Typbeschränkung ist nicht möglich. Gemäß der Dokumentation der Typeinschränkungen gibt es keine Einschränkung, die sowohl den nullbaren als auch den Referenztyp erfasst. Da Einschränkungen nur in einer Konjunktion kombiniert werden können, gibt es keine Möglichkeit, eine solche Einschränkung durch Kombination zu erstellen.

Sie können jedoch für Ihre Anforderungen auf einen Parameter ohne Einschränkungstyp zurückgreifen, da Sie immer nach == null suchen können. Wenn der Typ ein Werttyp ist, wird die Prüfung immer als falsch ausgewertet. Dann erhalten Sie möglicherweise die R # -Warnung "Möglicher Vergleich des Werttyps mit null", die nicht kritisch ist, solange die Semantik für Sie richtig ist.

Eine Alternative könnte sein, zu verwenden

object.Equals(value, default(T))

anstelle der Nullprüfung, da Standard (T) wobei die Klasse T: immer null ist. Dies bedeutet jedoch, dass Sie nicht unterscheiden können, ob ein nicht nullbarer Wert nie explizit oder nur auf seinen Standardwert festgelegt wurde.


Ich denke, dass das Problem darin besteht, zu überprüfen, ob dieser Wert nie festgelegt wurde. Anders als null scheint darauf hinzudeuten, dass der Wert initialisiert wurde.
Ryszard Dżegan

Dies macht den Ansatz nicht ungültig, da Werttypen immer festgelegt werden (zumindest implizit auf ihren jeweiligen Standardwert).
Sven Amann

3

ich benutze

public class Foo<T> where T: struct
{
    private T? item;
}

-2
    public class Foo<T>
    {
        private T item;

        public Foo(T item)
        {
            this.item = item;
        }

        public bool IsNull()
        {
            return object.Equals(item, null);
        }
    }

    var fooStruct = new Foo<int?>(3);
        var b = fooStruct.IsNull();

        var fooStruct1 = new Foo<int>(3);
        b = fooStruct1.IsNull();

        var fooStruct2 = new Foo<int?>(null);
        b = fooStruct2.IsNull();

        var fooStruct3 = new Foo<string>("qqq");
        b = fooStruct3.IsNull();

        var fooStruct4 = new Foo<string>(null);
        b = fooStruct4.IsNull();

Diese Eingabe ermöglicht, dass neues Foo <int> (42) und IsNull () false zurückgeben, was zwar semantisch korrekt, aber nicht besonders aussagekräftig ist.
RJ Lohan

1
42 ist "Die Antwort auf die ultimative Frage des Lebens, des Universums und alles". Einfach ausgedrückt: IsNull für jeden int-Wert gibt false zurück (auch für den Wert 0).
Ryszard Dżegan
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.