it-swarm-es.tech

Comparación nula o por defecto de un argumento genérico en C #

Tengo un método genérico definido así:

public void MyMethod<T>(T myArgument)

Lo primero que quiero hacer es verificar si el valor de myArgument es el valor predeterminado para ese tipo, algo como esto:

if (myArgument == default(T))

Pero esto no se compila porque no he garantizado que T implementará el operador ==. Así que cambié el código a esto:

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

Ahora esto se compila, pero fallará si myArgument es nulo, que es parte de lo que estoy probando. Puedo agregar un cheque nulo explícito como este:

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

Ahora esto me parece redundante. ReSharper incluso sugiere que cambie myArgument == null part a myArgument == default (T), que es donde comencé. ¿Hay una mejor manera de resolver este problema?

Necesito apoyo ambos Tipos de referencias y tipos de valores.

252
Stefan Moser

Para evitar el boxeo, la mejor manera de comparar genéricos para la igualdad es con EqualityComparer<T>.Default. Esto respeta IEquatable<T> (sin boxeo) así como object.Equals, y maneja todos los matices "levantados" de Nullable<T>. Por lo tanto:

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

Esto coincidirá con:

  • nulo para las clases
  • nulo (vacío) para Nullable<T>
  • cero/falso/etc para otras estructuras
503
Marc Gravell

Qué tal esto:

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

El uso del método static object.Equals() evita la necesidad de que usted haga null. La calificación explícita de la llamada con object. probablemente no sea necesaria dependiendo de su contexto, pero normalmente prefijo static calls con el nombre de tipo solo para hacer que el código sea más soluble.

114
Kent Boogaart

Pude localizar un artículo de Microsoft Connect que trata este problema con cierto detalle:

Desafortunadamente, este comportamiento es por diseño y no hay una solución fácil para habilitar el uso de parámetros de tipo que puedan contener tipos de valor.

Si se sabe que los tipos son tipos de referencia, la sobrecarga predeterminada de las variables de pruebas de objeto definidas para la igualdad de referencia, aunque un tipo puede especificar su propia sobrecarga personalizada. El compilador determina qué sobrecarga usar según el tipo estático de la variable (la determinación no es polimórfica). Por lo tanto, si cambia su ejemplo para restringir el parámetro de tipo genérico T a un tipo de referencia no sellado (como Excepción), el compilador puede determinar la sobrecarga específica que se utilizará y el siguiente código se compilaría:

public class Test<T> where T : Exception

Si se sabe que los tipos son tipos de valor, realiza pruebas de igualdad de valor específicas basadas en los tipos exactos utilizados. No hay una buena comparación "predeterminada" aquí, ya que las comparaciones de referencia no son significativas en los tipos de valor y el compilador no puede saber qué comparación de valor específico emitir. El compilador podría emitir una llamada a ValueType.Equals (Objeto), pero este método utiliza la reflexión y es bastante ineficiente en comparación con las comparaciones de valores específicos. Por lo tanto, incluso si tuviera que especificar una restricción de tipo de valor en T, no hay nada razonable para que el compilador genere aquí:

public class Test<T> where T : struct

En el caso que presentó, donde el compilador ni siquiera sabe si T es un valor o un tipo de referencia, tampoco hay nada que generar que sea válido para todos los tipos posibles. Una comparación de referencia no sería válida para los tipos de valor y algún tipo de comparación de valor sería inesperada para los tipos de referencia que no se sobrecargan.

Esto es lo que puedes hacer ...

He validado que ambos métodos funcionan para una comparación genérica de tipos de referencia y valor:

object.Equals(param, default(T))

o

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

Para hacer comparaciones con el operador "==" necesitará usar uno de estos métodos:

Si todos los casos de T se derivan de una clase base conocida, puede informar al compilador usando restricciones de tipo genérico.

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

El compilador entonces reconoce cómo realizar operaciones en MyBase y no lanzará el "Operador '==' no se puede aplicar a los operandos de tipo 'T' y 'T'" error que está viendo ahora.

Otra opción sería restringir T a cualquier tipo que implemente IComparable.

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

Y luego use el método CompareTo definido por la interfaz IComparable .

24
Eric Schoonover

Prueba esto:

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

eso debería compilar, y hacer lo que quieras.

18

(Editado)

Marc Gravell tiene la mejor respuesta, pero quería publicar un fragmento de código simple que elaboré para demostrarlo. Simplemente ejecute esto en una aplicación de consola C # simple:

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

Una cosa más: ¿puede alguien con VS2008 probar esto como un método de extensión? Estoy atascado con el 2005 aquí y tengo curiosidad por ver si eso sería permitido.


Editar: Aquí está cómo hacerlo funcionar como un método de extensión:

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));
    }
}
7
Joel Coehoorn

Para manejar todos los tipos de T, incluso cuando T es un tipo primitivo, deberá compilar ambos métodos de comparación:

    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;
    }
6
Nick Farina

Va a haber un problema aquí.

Si va a permitir que esto funcione para cualquier tipo, el valor predeterminado (T) siempre será nulo para los tipos de referencia y 0 (o estructura completa de 0) para los tipos de valor.

Sin embargo, probablemente este no sea el comportamiento que estás buscando. Si desea que esto funcione de una manera genérica, probablemente necesite usar la reflexión para verificar el tipo de T y manejar tipos de valores diferentes a los tipos de referencia.

Alternativamente, podría poner una restricción de interfaz en esto, y la interfaz podría proporcionar una manera de verificar el valor predeterminado de la clase/estructura.

2
Reed Copsey

Creo que probablemente deba dividir esta lógica en dos partes y comprobar primero si hay nulos.

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

En el método IsNull, confiamos en el hecho de que los objetos ValueType no pueden ser nulos por definición, por lo que si el valor es una clase que se deriva de ValueType, ya sabemos que no es nulo. Por otro lado, si no es un tipo de valor, podemos comparar el valor de conversión de un objeto con un valor nulo. Podríamos evitar la verificación contra ValueType yendo directamente a una conversión a objeto, pero eso significaría que un tipo de valor quedaría enmarcado, algo que probablemente queremos evitar ya que implica que se crea un nuevo objeto en el montón.

En el método IsNullOrEmpty, estamos comprobando el caso especial de una cadena. Para todos los demás tipos, estamos comparando el valor (que ya sabemos es no nulo) con su valor predeterminado, que para todos los tipos de referencia es nulo y para los tipos de valor generalmente es una forma de cero (si son integrales ).

Usando estos métodos, el siguiente código se comporta como es de esperar:

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) ...
}
1
Damian Powell

Yo suelo:

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