Contrainte de type générique C # pour tout ce qui peut être null

111

Donc j'ai cette classe:

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

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

}

Maintenant, je recherche une contrainte de type qui me permet d'utiliser tout comme paramètre de type qui peut l'être null. Cela signifie tous les types de référence, ainsi que tous les types Nullable( T?):

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

devrait être possible.

Utiliser classcomme contrainte de type me permet uniquement d'utiliser les types de référence.

Informations supplémentaires: J'écris une application de tuyaux et de filtres et je souhaite utiliser une nullréférence comme dernier élément qui passe dans le pipeline, afin que chaque filtre puisse s'arrêter correctement, faire un nettoyage, etc.

jkammerer
la source
1
@Tim qui ne permet pas les Nullables
Rik
Ce lien peut vous aider: social.msdn.microsoft.com/Forums/en-US/…
Réda Mattar
2
Il n'est pas possible de le faire directement. Peut-être pouvez-vous nous en dire plus sur votre scénario? Ou peut-être pourriez-vous utiliser IFoo<T>comme type de travail et créer des instances via une méthode d'usine? Cela pourrait fonctionner.
Jon
Je ne sais pas pourquoi vous voudriez ou auriez besoin de contraindre quelque chose de cette façon. Si votre seule intention est de transformer "if x == null" en if x.IsNull () "cela semble inutile et peu intuitif pour les 99,99% de développeurs qui sont habitués à l'ancienne syntaxe. Le compilateur ne vous laissera pas faire" if (int) x == null "de toute façon, donc vous êtes déjà couvert.
RJ Lohan
1
Ceci est assez largement discuté sur SO. stackoverflow.com/questions/209160/… et stackoverflow.com/questions/13794554/…
Maxim Gershkovich

Réponses:

22

Si vous êtes prêt à effectuer une vérification à l'exécution dans le constructeur de Foo plutôt que d'avoir une vérification à la compilation, vous pouvez vérifier si le type n'est pas un type référence ou Nullable, et lever une exception si c'est le cas.

Je me rends compte que seule une vérification à l'exécution peut être inacceptable, mais juste au cas où:

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

Ensuite, le code suivant se compile, mais le dernier ( foo3) lève une exception dans le constructeur:

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());
Matthew Watson
la source
31
Si vous allez faire cela, assurez-vous de faire la vérification dans le constructeur statique , sinon vous ralentirez la construction de chaque instance de votre classe générique (inutilement)
Eamon Nerbonne
2
@EamonNerbonne Vous ne devez pas déclencher d'exceptions à partir de constructeurs statiques: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson
5
Les directives ne sont pas absolues. Si vous voulez cette vérification, vous allez devoir faire un compromis entre le coût d'une vérification d'exécution et le manque de maniabilité des exceptions dans un constructeur statique. Puisque vous implémentez vraiment un analyseur statique pour les pauvres ici, cette exception ne devrait jamais être lancée sauf pendant le développement. Enfin, même si vous voulez éviter à tout prix les exceptions de construction statique (imprudent), vous devez quand même faire autant de travail que possible de manière statique et aussi peu que possible dans le constructeur d'instance - par exemple en définissant un indicateur "isBorked" ou autre.
Eamon Nerbonne
Soit dit en passant, je ne pense pas que vous devriez essayer de faire cela du tout. Dans la plupart des cas, je préférerais simplement accepter cela comme une limitation C #, plutôt que d'essayer de travailler avec une abstraction fuyante et sujette aux pannes. Par exemple, une solution différente pourrait être de simplement exiger des classes, ou simplement d'exiger des structures (et de rendre em explicitement nullable) - ou de faire les deux et d'avoir deux versions. Ce n'est pas une critique de cette solution; c'est juste que ce problème ne peut pas être résolu correctement - à moins que vous ne soyez prêt à écrire un analyseur Roslyn personnalisé.
Eamon Nerbonne
1
Vous pouvez obtenir le meilleur des deux mondes - conservez un static bool isValidTypechamp que vous avez défini dans le constructeur statique, puis vérifiez simplement cet indicateur dans le constructeur d'instance et lancez s'il s'agit d'un type non valide afin que vous ne fassiez pas tout le travail de vérification à chaque fois que vous construisez une instance. J'utilise souvent ce modèle.
Mike Marynowski
20

Je ne sais pas comment implémenter l'équivalent de OR en génériques. Cependant je peux proposer d'utiliser le mot clé par défaut afin de créer null pour les types Nullable et 0 valeur pour les structures:

public class Foo<T>
{
    private T item;

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

Vous pouvez également implémenter votre version de Nullable:

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

Exemple:

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
    }
}
Ryszard Dżegan
la source
Le Nullable <T> d'origine dans le framework est une structure, pas une classe. Je ne pense pas que ce soit une bonne idée de créer un wrapper de type de référence qui imitera un type valeur.
Niall Connaughton
1
La première suggestion utilisant default est parfaite! Maintenant, mon modèle avec un type générique renvoyé peut renvoyer une valeur null pour les objets et la valeur par défaut pour les types intégrés.
Casey Anderson
13

Je suis tombé sur ce problème pour un cas plus simple de vouloir une méthode statique générique qui pourrait prendre n'importe quoi "nullable" (soit des types de référence ou Nullables), ce qui m'a amené à cette question sans solution satisfaisante. J'ai donc proposé ma propre solution qui était relativement plus facile à résoudre que la question posée par l'OP en ayant simplement deux méthodes surchargées, une qui prend a Tet a la contrainte where T : classet une autre qui prend a T?et a where T : struct.

J'ai alors réalisé que cette solution pouvait également être appliquée à ce problème pour créer une solution vérifiable au moment de la compilation en rendant le constructeur privé (ou protégé) et en utilisant une méthode de fabrique statique:

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

Maintenant, nous pouvons l'utiliser comme ceci:

        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

Si vous voulez un constructeur sans paramètre, vous n'obtiendrez pas la subtilité de la surcharge, mais vous pouvez toujours faire quelque chose comme ceci:

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

Et utilisez-le comme ceci:

        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

Il y a peu d'inconvénients à cette solution, l'un est que vous préférerez peut-être utiliser «nouveau» pour construire des objets. Une autre est que vous ne serez pas en mesure d'utiliser Foo<T>comme argument de type générique pour une contrainte de type de quelque chose comme: where TFoo: new(). Enfin, il y a le peu de code supplémentaire dont vous avez besoin ici, ce qui augmenterait surtout si vous avez besoin de plusieurs constructeurs surchargés.

Dave M
la source
8

Comme mentionné, vous ne pouvez pas effectuer de vérification à la compilation. Les contraintes génériques dans .NET font cruellement défaut et ne prennent pas en charge la plupart des scénarios.

Cependant, je considère que c'est une meilleure solution pour la vérification au moment de l'exécution. Il peut être optimisé au moment de la compilation JIT, car ce sont tous les deux des constantes.

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);
    }
}
Aidiakapi
la source
3

Une telle contrainte de type n'est pas possible. Selon la documentation des contraintes de type, il n'y a pas de contrainte qui capture à la fois les types Nullable et Reference. Puisque les contraintes ne peuvent être combinées que dans une conjonction, il n'y a aucun moyen de créer une telle contrainte par combinaison.

Vous pouvez, cependant, pour vos besoins, revenir à un paramètre de type non contraint, car vous pouvez toujours vérifier == null. Si le type est un type valeur, la vérification sera toujours évaluée à false. Ensuite, vous obtiendrez peut-être l'avertissement R # "Comparaison possible du type valeur avec null", ce qui n'est pas critique, tant que la sémantique vous convient.

Une alternative pourrait être d'utiliser

object.Equals(value, default(T))

au lieu du contrôle nul, depuis la valeur par défaut (T) où T: class est toujours nul. Cependant, cela signifie que vous ne pouvez pas distinguer le temps qu'une valeur non nullable n'a jamais été définie explicitement ou a simplement été définie sur sa valeur par défaut.

Sven Amann
la source
Je pense que le problème est de savoir comment vérifier que la valeur n'a jamais été définie. Différent de null semble indiquer que la valeur a été initialisée.
Ryszard Dżegan
Cela n'invalide pas l'approche, car les types valeur sont toujours définis (au moins implicitement à leur valeur par défaut respective).
Sven Amann
3

j'utilise

public class Foo<T> where T: struct
{
    private T? item;
}
ela
la source
-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();
SeeSharp
la source
Ce typage autorise new Foo <int> (42) et IsNull () retournera false, ce qui, bien que sémantiquement correct, n'est pas particulièrement significatif.
RJ Lohan
1
42 est "La réponse à la question ultime de la vie, de l'univers et de tout". En termes simples: IsNull pour chaque valeur int retournera false (même pour une valeur 0).
Ryszard Dżegan