Comparação nula ou padrão de argumento genérico em C #

288

Eu tenho um método genérico definido assim:

public void MyMethod<T>(T myArgument)

A primeira coisa que quero fazer é verificar se o valor de myArgument é o valor padrão para esse tipo, algo como isto:

if (myArgument == default(T))

Mas isso não é compilado porque não garanti que T implementará o operador ==. Então, mudei o código para isso:

if (myArgument.Equals(default(T)))

Agora isso compila, mas falhará se myArgument for nulo, o que é parte do que estou testando. Posso adicionar uma verificação nula explícita como esta:

if (myArgument == null || myArgument.Equals(default(T)))

Agora isso parece redundante para mim. O ReSharper está sugerindo que eu mude a parte nula myArgument == para myArgument == default (T), que é onde eu comecei. Existe uma maneira melhor de resolver este problema?

Eu preciso para apoiar ambos os tipos de referências e tipos de valor.

Stefan Moser
fonte
O C # agora suporta Operadores Condicionais Nulos , que é o açúcar sintático do último exemplo que você fornece. Seu código se tornaria if (myArgument?.Equals( default(T) ) != null ).
precisa saber é o seguinte
1
@ wizard07KSU Isso não funciona para tipos de valor, isto é, avalia trueem qualquer caso, porque Equalssempre será chamado para tipos de valor, pois myArgumentnão pode ser nullnesse caso e o resultado de Equals(um booleano) nunca será null.
Jasper
Igualmente valioso quase duplicado (portanto, não está votando para fechar): O operador == não pode ser aplicado a tipos genéricos em C #?
GSerg

Respostas:

583

Para evitar o boxe, a melhor maneira de comparar os genéricos pela igualdade é com EqualityComparer<T>.Default. Isso respeita IEquatable<T>(sem boxe) e também object.Equalslida com todas as Nullable<T>nuances "levantadas". Conseqüentemente:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

Isso corresponderá a:

  • nulo para classes
  • nulo (vazio) para Nullable<T>
  • zero / falso / etc para outras estruturas
Marc Gravell
fonte
28
Uau, que deliciosamente obscuro! Esse é definitivamente o caminho a seguir, parabéns.
24412 Nick Farina
1
Definitivamente a melhor resposta. Nenhuma linha ondulada no meu código após a reescrita para usar esta solução.
Nathan Ridley
13
Ótima resposta! Ainda melhor é a adição de um método de extensão para esta linha de código para que você pode ir obj.IsDefaultForType ()
rikoe
2
@nawfal, no caso de Person, p1.Equals(p2)dependeria de sua implementação IEquatable<Person>na API pública ou via implementação explícita - ou seja, o compilador pode ver um Equals(Person other)método público . Contudo; em genéricos , a mesma IL é usada para todos T; um T1que implementa IEquatable<T1>precisa ser tratado de forma idêntica a um T2que não - portanto não, ele não localizará um Equals(T1 other)método, mesmo que exista em tempo de execução. Nos dois casos, também há o nullque pensar (qualquer um dos objetos). Então, com os genéricos, eu usaria o código que eu publiquei.
Marc Gravell
5
Não consigo decidir se essa resposta me afastou ou mais perto da loucura. 1
Steven Liekens
118

Que tal agora:

if (object.Equals(myArgument, default(T)))
{
    //...
}

O uso do static object.Equals()método evita a necessidade de você fazer a nullverificação você mesmo. Qualificar explicitamente a chamada object.provavelmente não é necessário, dependendo do seu contexto, mas normalmente prefixo as staticchamadas com o nome do tipo apenas para tornar o código mais solúvel.

Kent Boogaart
fonte
2
Você pode até soltar o "objeto". parte, pois é redundante. if (Equals (myArgument, default (T)))
Stefan Moser
13
É verdade que normalmente é, mas pode não depender do contexto. Pode haver um método Equals () de instância que usa dois argumentos. Eu tendem a prefixar explicitamente todas as chamadas estáticas com o nome da classe, apenas para facilitar a leitura do código.
Kent Boogaart 15/09/08
8
Necessidade de nota que ele fará com que o boxe e, em alguns casos, pode ser importante
nightcoder
2
Para mim, isso não funciona ao usar números inteiros que já estão em caixas. Porque será então um objeto e o padrão para o objeto é nulo em vez de 0.
riezebosch
28

Consegui localizar um artigo do Microsoft Connect que discute esse problema com alguns detalhes:

Infelizmente, esse comportamento ocorre por design e não há uma solução fácil para habilitar o uso de parâmetros de tipo que podem conter tipos de valor.

Se os tipos são conhecidos por serem tipos de referência, a sobrecarga padrão de definido no objeto testa variáveis ​​quanto à igualdade de referência, embora um tipo possa especificar sua própria sobrecarga customizada. O compilador determina qual sobrecarga usar com base no tipo estático da variável (a determinação não é polimórfica). Portanto, se você alterar seu exemplo para restringir o parâmetro de tipo genérico T para um tipo de referência não selado (como Exceção), o compilador poderá determinar a sobrecarga específica a ser usada e o seguinte código será compilado:

public class Test<T> where T : Exception

Se os tipos são conhecidos como tipos de valor, executa testes de igualdade de valor específicos com base nos tipos exatos usados. Não há boa comparação "padrão" aqui, pois as comparações de referência não são significativas nos tipos de valor e o compilador não pode saber qual comparação de valor específica a ser emitida. O compilador pode emitir uma chamada para ValueType.Equals (Object), mas esse método usa reflexão e é bastante ineficiente comparado às comparações de valores específicas. Portanto, mesmo se você especificasse uma restrição de tipo de valor em T, não há nada razoável para o compilador gerar aqui:

public class Test<T> where T : struct

No caso apresentado, onde o compilador nem sabe se T é um tipo de valor ou referência, da mesma forma, não há nada para gerar que seja válido para todos os tipos possíveis. Uma comparação de referência não seria válida para tipos de valor e algum tipo de comparação de valor seria inesperado para tipos de referência que não sobrecarregam.

Aqui está o que você pode fazer ...

Eu validei que ambos os métodos funcionam para uma comparação genérica dos tipos de referência e valor:

object.Equals(param, default(T))

ou

EqualityComparer<T>.Default.Equals(param, default(T))

Para fazer comparações com o operador "==", você precisará usar um destes métodos:

Se todos os casos de T derivarem de uma classe base conhecida, você poderá informar o compilador usando restrições de tipo genérico.

public void MyMethod<T>(T myArgument) where T : MyBase

O compilador reconhece como executar operações no MyBase e não lançará o "Operador '==' não pode ser aplicado aos operandos do tipo 'T' e 'T'" que você está vendo agora.

Outra opção seria restringir T a qualquer tipo que implemente IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

E, em seguida, use o CompareTométodo definido pela interface IComparable .

Eric Schoonover
fonte
4
"esse comportamento é por design e não há uma solução fácil para permitir o uso de parâmetros de tipo que podem conter tipos de valor". Na verdade, a Microsoft está errada. Existe uma solução fácil: a MS deve estender o código de operação ceq para funcionar em tipos de valor como um operador bit a bit. Eles poderiam fornecer um intrínseco que simplesmente usa esse código de operação, por exemplo, object.BitwiseOrReferenceEquals <T> (valor, padrão (T)) que simplesmente usa ceq. Para ambos os tipos de valor e referência Esta seria verificar a igualdade bit a bit do valor (mas para tipos de referência, a igualdade bit a bit de referência é o mesmo que Object.ReferenceEquals)
Qwertie
1
Eu acho que o link do Microsoft Connect que você queria era connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie 07/07/11
18

Tente o seguinte:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

isso deve compilar e fazer o que você quiser.

Lasse V. Karlsen
fonte
O <code> padrão (T) </code> não é redundante? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> deve fazer o truque.
Joshcodes
2
1) você tentou e 2) com o que você está comparando, o objeto comparador? O Equalsmétodo IEqualityComparerleva dois argumentos, os dois objetos a serem comparados; portanto, não é redundante.
Lasse V. Karlsen
Isso é ainda melhor do que a resposta aceita IMHO, porque lida com boxe / unboxing e outros tipos. Veja esta resposta das perguntas "closed as dupe": stackoverflow.com/a/864860/210780
ashes999
7

(Editado)

Marc Gravell tem a melhor resposta, mas eu queria postar um trecho de código simples que trabalhei para demonstrá-lo. Basta executar isso em um aplicativo de console simples em C #:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

Mais uma coisa: alguém com o VS2008 pode tentar isso como um método de extensão? Estou preso em 2005 aqui e estou curioso para ver se isso seria permitido.


Edit: Aqui está como fazê-lo funcionar como um método de extensão:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}
Joel Coehoorn
fonte
3
Ele "funciona" como um método de extensão. O que é interessante, pois funciona mesmo se você disser o.IsDefault <object> () quando o for nulo. Assustador =) #
Nick Farina
6

Para lidar com todos os tipos de T, incluindo onde T é um tipo primitivo, você precisará compilar nos dois métodos de comparação:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }
Nick Farina
fonte
1
Observe que a função foi alterada para aceitar Func <T> e retornar T, que eu acho que foi acidentalmente omitido do código do questionador.
Nick Farina
Parece que ReSharper está brincando comigo. Não percebeu seu aviso sobre uma possível comparação entre um tipo de valor e nulo não era um aviso do compilador.
Nathan Ridley
2
FYI: Se T for um tipo de valor, a comparação contra nulo será tratada como sempre falsa pelo jitter.
Eric Lippert
Faz sentido - o tempo de execução estará comparando um ponteiro com um tipo de valor. Entretanto, a verificação Equals () funciona nesse caso (curiosamente, pois parece uma linguagem muito dinâmica dizer 5.Equals (4), que é compilado).
Nick Farina
2
Veja a resposta do EqualityComparer <T> para uma alternativa que não envolva boxe e
Marc Gravell
2

Vai haver um problema aqui -

Se você permitir que isso funcione para qualquer tipo, o padrão (T) sempre será nulo para os tipos de referência e 0 (ou estrutura cheia de 0) para os tipos de valor.

Provavelmente, esse não é o comportamento que você procura. Se você deseja que isso funcione de maneira genérica, provavelmente precisará usar a reflexão para verificar o tipo de T e manipular tipos de valores diferentes dos tipos de referência.

Como alternativa, você pode colocar uma restrição de interface nisso, e a interface pode fornecer uma maneira de verificar o padrão da classe / estrutura.

Reed Copsey
fonte
1

Eu acho que você provavelmente precisará dividir essa lógica em duas partes e verificar se há nulo primeiro.

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

No método IsNull, contamos com o fato de que os objetos ValueType não podem ser nulos por definição; portanto, se o valor for uma classe que deriva de ValueType, já sabemos que não é nulo. Por outro lado, se não for um tipo de valor, podemos apenas comparar o valor convertido com um objeto contra nulo. Poderíamos evitar a verificação do ValueType indo diretamente para uma conversão para o objeto, mas isso significaria que um tipo de valor seria encaixotado, o que é algo que provavelmente queremos evitar, pois implica que um novo objeto é criado no heap.

No método IsNullOrEmpty, estamos verificando o caso especial de uma sequência. Para todos os outros tipos, estamos comparando o valor (que já sabe que não é nulo) com o valor padrão, que para todos os tipos de referência é nulo e, para os tipos de valor, geralmente é uma forma de zero (se eles são integrais).

Usando esses métodos, o seguinte código se comporta conforme o esperado:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}
Damian Powell
fonte
1

Método de extensão com base na resposta aceita.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Uso:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Alterne com null para simplificar:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Uso:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }
dynamiclynk
fonte
0

Eu uso:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}
kofifus
fonte
-1

Não sei se isso funciona com seus requisitos ou não, mas você pode restringir T a um tipo que implemente uma interface como IComparable e, em seguida, use o método ComparesTo () dessa interface (que o IIRC suporta / lida com nulos) como este :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

Provavelmente, existem outras interfaces que você poderia usar também IEquitable, etc.

caryden
fonte
O OP está preocupado com NullReferenceException e você está garantindo a ele o mesmo.
Nawfal
-2

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

O operador '==' não pode ser aplicado a operandos do tipo 'T' e 'T'

Não consigo pensar em uma maneira de fazer isso sem o teste nulo explícito seguido pela invocação do método ou objeto Equals.Equals, conforme sugerido acima.

Você pode criar uma solução usando o System.Comparison, mas na verdade isso vai acabar com muito mais linhas de código e aumentar substancialmente a complexidade.

cfeduke
fonte
-3

Eu acho que você estava perto.

if (myArgument.Equals(default(T)))

Agora isso compila, mas falhará se myArgumentfor nulo, o que é parte do que estou testando. Posso adicionar uma verificação nula explícita como esta:

Você só precisa reverter o objeto no qual os iguais estão sendo chamados para uma abordagem elegante com segurança nula.

default(T).Equals(myArgument);
Scott McKay
fonte
Eu estava pensando exatamente a mesma coisa.
Chris Gessler 23/03
6
o padrão (T) de um tipo de referência é nulo e resulta em uma NullReferenceException garantida.
Stefan Steinegger