Comparación nula o predeterminada de argumento genérico en C #

288

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 implemente el operador ==. Entonces cambié el código a esto:

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

Ahora esto se compila, pero fallará si myArgument es nulo, lo cual 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 la parte nula myArgument == en myArgument == default (T), que es donde comencé. ¿Hay una mejor manera de resolver este problema?

Tengo que apoyar ambos tipos de referencias y tipos de valor.

Stefan Moser
fuente
C # ahora admite operadores condicionales nulos , que es azúcar sintáctico para el último ejemplo que proporcione. Tu código se convertiría if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU
1
@ wizard07KSU Eso no funciona para los tipos de valor, es decir, se evalúa trueen cualquier caso porque Equalssiempre se llamará para los tipos de valor ya myArgumentque no puede ser nullen este caso y el resultado de Equals(un valor booleano) nunca lo será null.
Jasper
Igualmente valioso casi duplicado (para no votar para cerrar): ¿No se puede aplicar el operador == a los tipos genéricos en C #?
GSerg

Respuestas:

583

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) object.Equalsy maneja todos los Nullable<T>matices "elevados". Por lo tanto:

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

Esto coincidirá con:

  • nulo para clases
  • nulo (vacío) para Nullable<T>
  • cero / falso / etc. para otras estructuras
Marc Gravell
fuente
28
¡Guau, qué deliciosamente oscuro! Sin embargo, este es definitivamente el camino a seguir, felicitaciones.
Nick Farina
1
Definitivamente la mejor respuesta. No hay líneas onduladas en mi código después de reescribir para usar esta solución.
Nathan Ridley
13
¡Gran respuesta! Aún mejor es agregar un método de extensión para esta línea de código para que pueda ir obj.IsDefaultForType ()
rikoe
2
@nawfal en el caso de Person, p1.Equals(p2)dependería de si se implementa IEquatable<Person>en la API pública o mediante una implementación explícita, es decir, si el compilador puede ver un Equals(Person other)método público . Sin embargo; en genéricos , se usa la misma IL para todos T; Una T1implementación que IEquatable<T1>debe implementarse debe tratarse de manera idéntica a una T2que no lo hace, por lo que no, no detectará un Equals(T1 other)método, incluso si existe en tiempo de ejecución. En ambos casos, también hay nullque pensar (cualquier objeto). Entonces, con los genéricos, usaría el código que publiqué.
Marc Gravell
55
No puedo decidir si esta respuesta me alejó o me acercó a la locura. +1
Steven Liekens
118

Qué tal esto:

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

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

Kent Boogaart
fuente
2
Incluso puedes soltar el "objeto". parte ya que es redundante. if (Equals (myArgument, default (T)))
Stefan Moser
13
Es cierto que normalmente lo es, pero puede no depender del contexto. Puede haber un método Equals () de instancia que tome dos argumentos. Tiendo a prefijar explícitamente todas las llamadas estáticas con el nombre de la clase, aunque solo sea para que el código sea más fácil de leer.
Kent Boogaart
8
Es necesario tener en cuenta que provocará el boxeo y, en algunos casos, puede ser importante
nightcoder
2
Para mí, esto no funciona cuando utilizo enteros que ya están encuadrados. Porque entonces será un objeto y el valor predeterminado para el objeto es nulo en lugar de 0.
riezebosch
28

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

Desafortunadamente, este comportamiento es por diseño y no existe una solución fácil para permitir 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 definidas en el objeto prueba la igualdad de referencia, aunque un tipo puede especificar su propia sobrecarga personalizada. El compilador determina qué sobrecarga usar en función del 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 Exception), el compilador puede determinar la sobrecarga específica a 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. Aquí no hay una buena comparación "predeterminada" 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ífica emitir. El compilador podría emitir una llamada a ValueType.Equals (Object) 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 "==", deberá utilizar uno de estos métodos:

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

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

El compilador luego reconoce cómo realizar operaciones MyBasey no arrojará 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 CompareTométodo definido por la interfaz IComparable .

Eric Schoonover
fuente
44
"Este comportamiento es por diseño y no existe una solución fácil para permitir el uso de parámetros de tipo que puedan contener tipos de valor". En realidad, Microsoft está equivocado. Hay una solución fácil: MS debería extender el código de operación ceq para que funcione en los tipos de valor como operador bit a bit. Entonces podrían proporcionar un intrínseco que simplemente usa este código de operación, por ejemplo, object.BitwiseOrReferenceEquals <T> (valor, predeterminado (T)) que simplemente usa ceq. Para ambos tipos de valores de referencia y esto sería comprobar la igualdad bit a bit del valor (pero para los tipos de referencia, la igualdad bit a bit de referencia es el mismo que object.ReferenceEquals)
Qwertie
1
Creo que el enlace de Microsoft Connect que quería era connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie
18

Prueba esto:

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

eso debería compilar y hacer lo que quieras.

Lasse V. Karlsen
fuente
¿No es el <code> default (T) </code> redundante? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> debería hacer el truco.
Joshcodes
2
1) ¿lo intentaste y 2) con qué estás comparando, el objeto comparador? El Equalsmétodo de IEqualityComparertoma dos argumentos, los dos objetos para comparar, así que no, no es redundante.
Lasse V. Karlsen
Esto es incluso mejor que la respuesta aceptada en mi humilde opinión porque maneja el boxeo / unboxing y otros tipos. Vea la respuesta a las preguntas de "cerrado como dupe": stackoverflow.com/a/864860/210780
ashes999
7

(Editado)

Marc Gravell tiene la mejor respuesta, pero quería publicar un fragmento de código simple que trabajé 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: ¿alguien con VS2008 puede probar esto como un método de extensión? Estoy atrapado con 2005 aquí y tengo curiosidad por ver si eso estaría permitido.


Editar: Aquí se explica cómo hacer que funcione 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));
    }
}
Joel Coehoorn
fuente
3
Hace "trabajo" como un método de extensión. Lo cual es interesante ya que funciona incluso si dice o.IsDefault <object> () cuando o es nulo. Miedo =)
Nick Farina
6

Para manejar todos los tipos de T, incluso cuando T es un tipo primitivo, deberá compilar en 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;
    }
Nick Farina
fuente
1
Tenga en cuenta que la función se ha cambiado para aceptar Func <T> y devolver T, que creo que se omitió accidentalmente del código del interlocutor.
Nick Farina
Parece que ReSharper está jugando conmigo. No me di cuenta de que su advertencia sobre una posible comparación entre un tipo de valor y nulo no era una advertencia del compilador.
Nathan Ridley
2
FYI: Si T resulta ser un tipo de valor, la comparación con nulo será tratada como siempre falsa por la fluctuación de fase.
Eric Lippert
Tiene sentido: el tiempo de ejecución comparará un puntero con un tipo de valor. Sin embargo, la comprobación Equals () funciona en ese caso (curiosamente, ya que parece un lenguaje muy dinámico decir 5.Equals (4) que sí se compila).
Nick Farina
2
Vea la respuesta EqualityComparer <T> para una alternativa que no involucra boxeo y et
Marc Gravell
2

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 llena de 0) para los tipos de valor.

Sin embargo, este probablemente no sea el comportamiento que buscas. Si desea que esto funcione de 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 forma de verificar el valor predeterminado de la clase / estructura.

Reed Copsey
fuente
1

Creo que probablemente necesites dividir esta lógica en dos partes y verificar primero si es nulo.

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, entonces podemos comparar el valor emitido con un objeto contra nulo. Podríamos evitar la comprobación de ValueType yendo directamente a un objeto de conversión, pero eso significaría que un tipo de valor quedaría encuadrado, lo que probablemente es algo que queremos evitar, ya que implica que se crea un nuevo objeto en el montón.

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

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

Método de extensión basado en la respuesta aceptada.

   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 con nulo 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
fuente
0

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;
  }
}
kofifus
fuente
-1

No sé si esto funciona con sus requisitos o no, pero podría restringir a T para que sea un Tipo que implemente una interfaz como IComparable y luego usar el método ComparesTo () desde esa interfaz (que IIRC admite / maneja nulos) como este :

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

Probablemente hay otras interfaces que podría usar también IEquitable, etc.

Carryden
fuente
OP está preocupado por NullReferenceException y le está garantizando lo mismo.
nawfal
-2

@ilitirit:

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

El operador '==' no se puede aplicar a operandos de tipo 'T' y 'T'

No puedo pensar en una manera de hacer esto sin la prueba nula explícita seguida de invocar el método u objeto Equals. Igual como se sugirió anteriormente.

Puede idear una solución usando System.Comparison pero realmente eso terminará con más líneas de código y aumentará la complejidad sustancialmente.

cfeduke
fuente
-3

Creo que estabas cerca.

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

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

Solo necesita revertir el objeto en el que se llama a los iguales para un enfoque elegante de seguridad nula.

default(T).Equals(myArgument);
Scott McKay
fuente
Estaba pensando exactamente lo mismo.
Chris Gessler
66
el valor predeterminado (T) de un tipo de referencia es nulo y da como resultado una NullReferenceException garantizada.
Stefan Steinegger