Vincolo di tipo generico C # per tutto ciò che annulla


111

Quindi ho questa classe:

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

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

}

Ora sto cercando un vincolo di tipo che mi consenta di utilizzare tutto come parametro di tipo che può essere null. Ciò significa tutti i tipi di riferimento, così come tutti i tipi Nullable( T?):

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

dovrebbe essere possibile.

L'utilizzo classcome vincolo di tipo mi consente solo di utilizzare i tipi di riferimento.

Informazioni aggiuntive: Sto scrivendo un'applicazione per pipe e filtri e desidero utilizzare un nullriferimento come ultimo elemento che passa nella pipeline, in modo che ogni filtro possa spegnersi correttamente, eseguire la pulizia, ecc ...


1
@Tim che non consente Nullables
Rik

Questo collegamento può aiutarti: social.msdn.microsoft.com/Forums/en-US/…
Réda Mattar

2
Non è possibile farlo direttamente. Forse puoi dirci di più sul tuo scenario? O forse potresti usarlo IFoo<T>come tipo di lavoro e creare istanze tramite un metodo factory? Potrebbe essere fatto funzionare.
Jon

Non sono sicuro del motivo per cui vorresti o avresti bisogno di vincolare qualcosa in questo modo. Se il tuo unico intento è trasformare "if x == null" in if x.IsNull () "questo sembra inutile e poco intuitivo per il 99,99% degli sviluppatori che sono abituati alla sintassi precedente. Il compilatore non te lo permette" if (int) x == null "comunque, quindi sei già coperto.
RJ Lohan

Risposte:


22

Se sei disposto a fare un controllo di runtime nel costruttore di Foo piuttosto che avere un controllo in fase di compilazione, puoi controllare se il tipo non è un riferimento o un tipo nullable e generare un'eccezione in questo caso.

Mi rendo conto che avere solo un controllo di runtime potrebbe essere inaccettabile, ma per ogni evenienza:

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

Quindi il codice seguente viene compilato, ma l'ultimo ( foo3) genera un'eccezione nel costruttore:

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
Se hai intenzione di farlo, assicurati di fare il controllo nel costruttore statico , altrimenti
rallenterai

2
@EamonNerbonne Non dovresti sollevare eccezioni da costruttori statici: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
Le linee guida non sono assolute. Se vuoi questo controllo, dovrai bilanciare il costo di un controllo di runtime rispetto alla non maneggevolezza delle eccezioni in un costruttore statico. Dal momento che qui stai davvero implementando un analizzatore statico per uomini poveri, questa eccezione non dovrebbe mai essere lanciata tranne durante lo sviluppo. Infine, anche se vuoi evitare eccezioni di costruzione statica a tutti i costi (imprudente), dovresti comunque fare quanto più lavoro possibile staticamente e il meno possibile nel costruttore dell'istanza, ad esempio impostando un flag "isBorked" o qualsiasi altra cosa.
Eamon Nerbonne

Per inciso, non penso che dovresti provare a farlo affatto. Nella maggior parte dei casi preferirei accettarlo come una limitazione di C #, piuttosto che provare a lavorare con un'astrazione che perde e soggetta a errori. Ad esempio, una soluzione diversa potrebbe essere quella di richiedere solo classi, o semplicemente richiedere struct (e rendere esplicitamente em nullable) - o fare entrambe le cose e avere due versioni. Non è una critica a questa soluzione; è solo che questo problema non può essere risolto bene, a meno che, cioè, tu non sia disposto a scrivere un analizzatore di roslyn personalizzato.
Eamon Nerbonne

1
Puoi ottenere il meglio da entrambi i mondi: mantieni un static bool isValidTypecampo che hai impostato nel costruttore statico, quindi controlla quel flag nel costruttore dell'istanza e lancia se è un tipo non valido in modo da non fare tutto il lavoro di controllo ogni volta che costruisci un caso. Uso spesso questo schema.
Mike Marynowski

20

Non so come implementare l'equivalente di OR nei generici. Tuttavia posso proporre di utilizzare la parola chiave predefinita per creare null per i tipi nullable e il valore 0 per le strutture:

public class Foo<T>
{
    private T item;

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

Puoi anche implementare la tua versione di 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;
    }
}

Esempio:

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

L'originale Nullable <T> nel framework è una struttura, non una classe. Non credo sia una buona idea creare un wrapper del tipo di riferimento che imiti un tipo di valore.
Niall Connaughton,

1
Il primo suggerimento usando l' impostazione predefinita è perfetto! Ora il mio modello con un tipo generico restituito può restituire un valore nullo per gli oggetti e il valore predefinito per i tipi incorporati.
Casey Anderson

13

Mi sono imbattuto in questo problema per un caso più semplice di volere un metodo statico generico che potesse accettare qualsiasi cosa "nullable" (tipi di riferimento o Nullables), il che mi ha portato a questa domanda senza una soluzione soddisfacente. Quindi ho trovato la mia soluzione che era relativamente più facile da risolvere rispetto alla domanda dichiarata dell'OP semplicemente avendo due metodi sovraccaricati, uno che accetta a Te ha il vincolo where T : classe un altro che richiede a T?e ha where T : struct.

Mi sono quindi reso conto che quella soluzione può essere applicata anche a questo problema per creare una soluzione controllabile in fase di compilazione rendendo il costruttore privato (o protetto) e utilizzando un metodo factory statico:

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

Ora possiamo usarlo in questo modo:

        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

Se vuoi un costruttore senza parametri, non otterrai la delicatezza del sovraccarico, ma puoi comunque fare qualcosa del genere:

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

E usalo in questo modo:

        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

Ci sono pochi svantaggi in questa soluzione, uno è che potresti preferire l'utilizzo di "nuovo" per costruire oggetti. Un altro è che non sarà in grado di utilizzare Foo<T>come un tipo di argomento generico per un vincolo tipo di qualcosa come: where TFoo: new(). Infine è il pezzo di codice extra di cui hai bisogno qui che aumenterebbe soprattutto se hai bisogno di più costruttori sovraccarichi.


8

Come accennato, non è possibile eseguire un controllo in fase di compilazione. I vincoli generici in .NET sono gravemente carenti e non supportano la maggior parte degli scenari.

Tuttavia, ritengo che questa sia una soluzione migliore per il controllo in fase di esecuzione. Può essere ottimizzato al momento della compilazione JIT, poiché sono entrambe costanti.

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

Un tale vincolo di tipo non è possibile. In base alla documentazione dei vincoli di tipo, non esiste un vincolo che acquisisca sia i tipi nullable che quelli di riferimento. Poiché i vincoli possono essere combinati solo in una congiunzione, non è possibile creare un tale vincolo mediante combinazione.

Puoi, tuttavia, per le tue esigenze ripiegare su un parametro di tipo non vincolante, poiché puoi sempre controllare == null. Se il tipo è un tipo di valore, il controllo restituirà sempre false. Quindi potresti ricevere l'avvertimento R # "Possibile confronto del tipo di valore con null", che non è critico, purché la semantica sia giusta per te.

Un'alternativa potrebbe essere quella di utilizzare

object.Equals(value, default(T))

invece del controllo nullo, poiché default (T) dove T: class è sempre nullo. Ciò, tuttavia, significa che non è possibile distinguere se un valore non annullabile non è mai stato impostato in modo esplicito o è stato semplicemente impostato sul valore predefinito.


Penso che il problema sia come controllare che il valore non sia mai stato impostato. Diverso da null sembra indicare che il valore è stato inizializzato.
Ryszard Dżegan

Ciò non invalida l'approccio, poiché i tipi di valore sono sempre impostati (almeno implicitamente sul rispettivo valore predefinito).
Sven Amann

3

Io uso

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

Questa digitazione consente a new Foo <int> (42) e IsNull () restituirà false, che, sebbene semanticamente corretta, non è particolarmente significativa.
RJ Lohan

1
42 è "La risposta all'ultima domanda sulla vita, l'universo e tutto". In poche parole: IsNull per ogni valore int restituirà false (anche per il valore 0).
Ryszard Dżegan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.