Restricción de tipo genérico de C # para todo lo que acepta valores NULL


111

Entonces tengo esta clase:

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

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

}

Ahora estoy buscando una restricción de tipo que me permita usar todo como parámetro de tipo que pueda ser null. Eso significa todos los tipos de referencia, así como todos los tipos Nullable( T?):

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

debería ser posible.

Usar classcomo restricción de tipo solo me permite usar los tipos de referencia.

Información adicional: estoy escribiendo una aplicación de tuberías y filtros, y quiero usar una nullreferencia como el último elemento que pasa a la tubería, de modo que cada filtro pueda apagarse bien, hacer limpieza, etc.


1
@Tim que no permite Nullables
Rik


2
No es posible hacer esto directamente. ¿Quizás puedas contarnos más sobre tu escenario? ¿O quizás podría usarlo IFoo<T>como tipo de trabajo y crear instancias a través de un método de fábrica? Eso podría funcionar.
Jon

No estoy seguro de por qué querría o necesitaría restringir algo de esta manera. Si su única intención es convertir "if x == null" en if x.IsNull () ", esto parece inútil y poco intuitivo para el 99,99% de los desarrolladores que están acostumbrados a la sintaxis anterior. El compilador no le permitirá hacerlo" if (int) x == null "de todos modos, entonces ya está cubierto.
RJ Lohan

Respuestas:


22

Si está dispuesto a realizar una verificación en tiempo de ejecución en el constructor de Foo en lugar de realizar una verificación en tiempo de compilación, puede verificar si el tipo no es una referencia o un tipo anulable, y lanzar una excepción si ese es el caso.

Me doy cuenta de que solo tener una verificación de tiempo de ejecución puede ser inaceptable, pero por si acaso:

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

Luego, el siguiente código se compila, pero el último ( foo3) arroja una excepción en el constructor:

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
Si va a hacer esto, asegúrese de hacer la verificación en el constructor estático , de lo contrario, ralentizará la construcción de cada instancia de su clase genérica (innecesariamente)
Eamon Nerbonne

2
@EamonNerbonne No debe generar excepciones de constructores estáticos: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
Las pautas no son absolutas. Si desea esta verificación, tendrá que compensar el costo de una verificación en tiempo de ejecución frente a la falta de manejo de las excepciones en un constructor estático. Dado que realmente está implementando un analizador estático para personas pobres aquí, esta excepción nunca debe lanzarse excepto durante el desarrollo. Finalmente, incluso si desea evitar las excepciones de construcción estática a toda costa (imprudente), debe seguir haciendo tanto trabajo como sea posible de forma estática y tan poco como sea posible en el constructor de la instancia, por ejemplo, estableciendo una marca "isBorked" o lo que sea.
Eamon Nerbonne

Por cierto, no creo que debas intentar hacer esto en absoluto. En la mayoría de las circunstancias, preferiría aceptar esto como una limitación de C #, en lugar de intentar trabajar con una abstracción propensa a fallas y con fugas. Por ejemplo, una solución diferente podría ser simplemente requerir clases, o simplemente requerir estructuras (y hacer explícitamente emulable), o hacer ambas cosas y tener dos versiones. Eso no es una crítica a esta solución; es solo que este problema no se puede resolver bien, a menos que, es decir, esté dispuesto a escribir un analizador Roslyn personalizado.
Eamon Nerbonne

1
Puede obtener lo mejor de ambos mundos: mantenga un static bool isValidTypecampo que estableció en el constructor estático, luego simplemente verifique esa marca en el constructor de instancia y arroje si es un tipo no válido para que no esté haciendo todo el trabajo de verificación cada vez que construya una instancia. Utilizo este patrón a menudo.
Mike Marynowski

20

No sé cómo implementar equivalente a OR en genéricos. Sin embargo, puedo proponer usar la palabra clave predeterminada para crear un valor nulo para los tipos que aceptan valores NULL y un valor 0 para las estructuras:

public class Foo<T>
{
    private T item;

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

También puede implementar su versión 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;
    }
}

Ejemplo:

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

El <T> Nullable original en el marco es una estructura, no una clase. No creo que sea una buena idea crear un contenedor de tipo de referencia que imite un tipo de valor.
Niall Connaughton

1
¡La primera sugerencia usando default es perfecta! Ahora mi plantilla con un tipo genérico que se devuelve puede devolver un valor nulo para los objetos y el valor predeterminado para los tipos integrados.
Casey Anderson

13

Me encontré con este problema por un caso más simple de querer un método estático genérico que pudiera tomar cualquier cosa "anulable" (ya sea tipos de referencia o Nullables), lo que me llevó a esta pregunta sin una solución satisfactoria. Así que se me ocurrió mi propia solución, que era relativamente más fácil de resolver que la pregunta indicada por el OP al tener simplemente dos métodos sobrecargados, uno que toma un Ty tiene la restricción where T : classy otro que toma unT? tiene where T : struct.

Luego me di cuenta de que esa solución también se puede aplicar a este problema para crear una solución que se pueda verificar en tiempo de compilación haciendo que el constructor sea privado (o protegido) y utilizando un método de fábrica estático:

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

Ahora podemos usarlo así:

        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 desea un constructor sin parámetros, no obtendrá la delicadeza de la sobrecarga, pero aún puede hacer algo como esto:

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

Y utilícelo así:

        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

Hay algunas desventajas en esta solución, una es que puede preferir usar 'nuevo' para construir objetos. Otra es que usted no será capaz de utilizar Foo<T>como un argumento de tipo genérico para una restricción tipo de algo como: where TFoo: new(). Por último, está el código extra que necesita aquí, que aumentaría especialmente si necesita varios constructores sobrecargados.


8

Como se mencionó, no puede tener una verificación en tiempo de compilación. Las restricciones genéricas en .NET son muy deficientes y no son compatibles con la mayoría de los escenarios.

Sin embargo, considero que esta es una mejor solución para la verificación del tiempo de ejecución. Se puede optimizar en tiempo de compilación JIT, ya que ambas son 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);
    }
}

3

Tal restricción de tipo no es posible. Según la documentación de las restricciones de tipo, no existe ninguna restricción que capture tanto los tipos que aceptan valores NULL como los de referencia. Dado que las restricciones solo se pueden combinar en una conjunción, no hay forma de crear tal restricción por combinación.

Sin embargo, para sus necesidades, puede recurrir a un parámetro de tipo sin restricción, ya que siempre puede verificar == null. Si el tipo es un tipo de valor, la comprobación siempre se evaluará como falsa. Entonces posiblemente obtendrá la advertencia de R # "Posible comparación del tipo de valor con nulo", que no es crítica, siempre que la semántica sea la adecuada para usted.

Una alternativa podría ser utilizar

object.Equals(value, default(T))

en lugar de la comprobación nula, ya que por defecto (T) donde T: clase siempre es nula. Esto, sin embargo, significa que no puede distinguir si un valor que no acepta valores NULL nunca se ha establecido explícitamente o simplemente se estableció en su valor predeterminado.


Creo que el problema es cómo comprobar que nunca se ha establecido ese valor. Diferente que nulo parece indicar que el valor se ha inicializado.
Ryszard Dżegan

Eso no invalida el enfoque, ya que los tipos de valor siempre se establecen (al menos implícitamente en su respectivo valor predeterminado).
Sven Amann

3

yo suelo

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

Esta escritura permite nuevo Foo <int> (42) e IsNull () devolverá falso, lo que, aunque semánticamente correcto, no es particularmente significativo.
RJ Lohan

1
42 es "La respuesta a la pregunta fundamental de la vida, el universo y todo". En pocas palabras: IsNull para cada valor int devolverá falso (incluso para el valor 0).
Ryszard Dżegan
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.