Restrição de tipo genérico C # para tudo anulável


111

Então, eu tenho esta aula:

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

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

}

Agora estou procurando uma restrição de tipo que me permita usar tudo como parâmetro de tipo que possa ser null. Isso significa todos os tipos de referência, bem como todos os tipos Nullable( T?):

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

deve ser possível.

Usar classcomo restrição de tipo apenas me permite usar os tipos de referência.

Informações Adicionais: Estou escrevendo um aplicativo de tubos e filtros e quero usar uma nullreferência como o último item que passa para o pipeline, para que cada filtro possa fechar bem, fazer limpeza, etc ...


1
@Tim que não permite Nullables
Rik


2
Não é possível fazer isso diretamente. Talvez você possa nos contar mais sobre seu cenário? Ou talvez você pudesse usar IFoo<T>como o tipo de trabalho e criar instâncias por meio de um método de fábrica? Isso poderia funcionar.
Jon

Não tenho certeza de por que você deseja ou precisa restringir algo dessa maneira. Se sua única intenção é transformar "if x == null" em if x.IsNull () ", isso parece sem sentido e não intuitivo para 99,99% dos desenvolvedores que estão acostumados com a sintaxe anterior. O compilador não permite" if (int) x == null "de qualquer maneira, então você já está coberto.
RJ Lohan

Respostas:


22

Se você estiver disposto a fazer uma verificação de tempo de execução no construtor de Foo em vez de ter uma verificação de tempo de compilação, você pode verificar se o tipo não é uma referência ou tipo anulável e lançar uma exceção se for o caso.

Eu percebo que apenas ter uma verificação de tempo de execução pode ser inaceitável, mas apenas no caso:

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

Em seguida, o código a seguir é compilado, mas o último ( foo3) lança uma exceção no construtor:

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 você for fazer isso, certifique-se de fazer a verificação no construtor estático , caso contrário, você retardará a construção de cada instância de sua classe genérica (desnecessariamente)
Eamon Nerbonne,

2
@EamonNerbonne Você não deve levantar exceções de construtores estáticos: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
As diretrizes não são absolutas. Se você quiser essa verificação, terá que compensar o custo de uma verificação de tempo de execução pela falta de manipulação de exceções em um construtor estático. Já que você está realmente implementando um analisador estático de pobre aqui, essa exceção nunca deve ser lançada, exceto durante o desenvolvimento. Finalmente, mesmo se você quiser evitar exceções de construção estática a todo custo (imprudente), você ainda deve fazer o máximo de trabalho possível estaticamente e o mínimo possível no construtor de instância - por exemplo, definindo um sinalizador "isBorked" ou qualquer outra coisa.
Eamon Nerbonne

A propósito, não acho que você deva tentar fazer isso. Na maioria das circunstâncias, prefiro aceitar isso apenas como uma limitação do C #, em vez de tentar trabalhar com uma abstração com vazamento e propensa a falhas. Por exemplo, uma solução diferente pode ser apenas exigir classes, ou apenas exigir structs (e torná-los explicitamente nulos) - ou fazer ambos e ter duas versões. Isso não é uma crítica a esta solução; só que esse problema não pode ser bem resolvido - a menos que você queira escrever um analisador roslyn personalizado.
Eamon Nerbonne

1
Você pode obter o melhor dos dois mundos - mantenha um static bool isValidTypecampo que você definiu no construtor estático, então apenas verifique o sinalizador no construtor de instância e jogue se for um tipo inválido, então você não fará todo o trabalho de verificação cada vez que construir uma instância. Eu uso esse padrão com frequência.
Mike Marynowski

20

Não sei como implementar equivalente a OR em genéricos. No entanto, posso propor o uso de palavra-chave padrão a fim de criar nulo para tipos anuláveis ​​e valor 0 para estruturas:

public class Foo<T>
{
    private T item;

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

Você também pode implementar sua versão 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;
    }
}

Exemplo:

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

O Nullable <T> original na estrutura é uma estrutura, não uma classe. Não acho uma boa ideia criar um wrapper de tipo de referência que imite um tipo de valor.
Niall Connaughton

1
A primeira sugestão de usar padrão é perfeita! Agora, meu modelo com um tipo genérico sendo retornado pode retornar um nulo para objetos e o valor padrão para tipos integrados.
Casey Anderson

13

Corri para esse problema para um caso mais simples de querer um método estático genérico que pudesse tomar qualquer coisa "anulável" (tipos de referência ou Nullables), o que me trouxe a esta questão sem solução satisfatória. Então eu vim com minha própria solução que era relativamente mais fácil de resolver do que a pergunta declarada do OP, simplesmente tendo dois métodos sobrecarregados, um que leva a Te tem a restrição where T : classe outro que leva a T?e tem where T : struct.

Então percebi que essa solução também pode ser aplicada a este problema para criar uma solução que seja verificável em tempo de compilação, tornando o construtor privado (ou protegido) e usando um 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);
        }
    }

Agora podemos usá-lo assim:

        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 você quiser um construtor sem parâmetros, não terá a delicadeza da sobrecarga, mas ainda pode fazer algo assim:

    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 use-o assim:

        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

Existem algumas desvantagens para esta solução, uma é que você pode preferir usar 'novo' para construir objetos. Outra é que você não será capaz de usar Foo<T>como um argumento de tipo genérico para um tipo de restrição de algo como: where TFoo: new(). Finalmente, está o código extra de que você precisa aqui, que aumentaria especialmente se você precisar de vários construtores sobrecarregados.


8

Conforme mencionado, você não pode ter uma verificação em tempo de compilação para ele. Faltam restrições genéricas no .NET e não oferecem suporte à maioria dos cenários.

No entanto, considero esta a melhor solução para verificação em tempo de execução. Ele pode ser otimizado em tempo de compilação JIT, uma vez que ambos são 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

Essa restrição de tipo não é possível. De acordo com a documentação das restrições de tipo, não há restrição que capture os tipos anuláveis ​​e de referência. Como as restrições só podem ser combinadas em uma conjunção, não há como criar essa restrição por combinação.

Você pode, no entanto, para suas necessidades, recorrer a um parâmetro de tipo irrestrito, uma vez que você sempre pode verificar == null. Se o tipo for um tipo de valor, a verificação sempre será avaliada como falsa. Em seguida, você possivelmente obterá o aviso R # "Possível comparação do tipo de valor com nulo", o que não é crítico, contanto que a semântica seja adequada para você.

Uma alternativa poderia ser usar

object.Equals(value, default(T))

em vez da verificação de nulo, pois o padrão (T) onde T: classe é sempre nulo. Isso, entretanto, significa que você não pode distinguir se um valor não anulável nunca foi definido explicitamente ou apenas foi definido com seu valor padrão.


Acho que o problema é como verificar se o valor nunca foi definido. Diferente de nulo parece apontar que o valor foi inicializado.
Ryszard Dżegan

Isso não invalida a abordagem, uma vez que os tipos de valor são sempre definidos (pelo menos implicitamente com seus respectivos valores padrão).
Sven Amann

3

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

Essa tipificação permite new Foo <int> (42) e IsNull () retornará false, o que, embora semanticamente correto, não é particularmente significativo.
RJ Lohan

1
42 é "A resposta para a questão fundamental da vida, do universo e de tudo". Simplificando: IsNull para cada valor int retornará false (mesmo para valor 0).
Ryszard Dżegan
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.