¿No se puede aplicar el operador == a los tipos genéricos en C #?

326

De acuerdo con la documentación del ==operador en MSDN ,

Para los tipos de valores predefinidos, el operador de igualdad (==) devuelve verdadero si los valores de sus operandos son iguales, de lo contrario, falso. Para los tipos de referencia que no sean cadenas, == devuelve verdadero si sus dos operandos se refieren al mismo objeto. Para el tipo de cadena, == compara los valores de las cadenas. Los tipos de valores definidos por el usuario pueden sobrecargar el operador == (ver operador). También pueden los tipos de referencia definidos por el usuario, aunque por defecto == se comporta como se describe anteriormente para los tipos de referencia predefinidos y definidos por el usuario.

Entonces, ¿por qué este fragmento de código no se compila?

bool Compare<T>(T x, T y) { return x == y; }

Me sale el error Operador '==' no se puede aplicar a operandos de tipo 'T' y 'T' . Me pregunto por qué, ya que entiendo que el ==operador está predefinido para todos los tipos.

Editar: Gracias a todos. Al principio no me di cuenta de que la declaración se refería solo a los tipos de referencia. También pensé que se proporciona una comparación bit a bit para todos los tipos de valores, lo que ahora sé que no es correcto.

Pero, en caso de que esté usando un tipo de referencia, ¿usaría el ==operador la comparación de referencia predefinida, o usaría la versión sobrecargada del operador si un tipo definiera uno?

Edición 2: a través de prueba y error, aprendimos que el ==operador usará la comparación de referencia predefinida cuando use un tipo genérico sin restricciones. En realidad, el compilador utilizará el mejor método que pueda encontrar para el argumento de tipo restringido, pero no buscará más. Por ejemplo, el siguiente código siempre se imprimirá true, incluso cuando Test.test<B>(new B(), new B())se llama:

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }
Hosam Aly
fuente
Vea mi respuesta nuevamente para la respuesta a su pregunta de seguimiento.
Giovanni Galbo
Puede ser útil comprender que incluso sin genéricos, hay algunos tipos para los cuales ==no está permitido entre dos operandos del mismo tipo. Esto es cierto para los structtipos (excepto los tipos "predefinidos") que no sobrecargan el operator ==. Como ejemplo simple, intente esto:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen
Continuando con mi viejo comentario. Por ejemplo (vea otro hilo ), con var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, entonces no puede verificar kvp1 == kvp2porque KeyValuePair<,>es una estructura, no es un tipo predefinido de C # y no sobrecarga el operator ==. Sin embargo, se proporciona un ejemplo var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;con el que no puede hacerlo e1 == e2(aquí tenemos la estructura anidada List<>.Enumerator(llamada "List`1+Enumerator[T]"por el tiempo de ejecución) que no se sobrecarga ==).
Jeppe Stig Nielsen
RE: "Entonces, ¿por qué este fragmento de código no se compila?" - Er ... porque no puedes devolver un boolde void...
BrainSlugs83
1
@ BrainSlugs83 ¡Gracias por atrapar un error de 10 años!
Hosam Aly

Respuestas:

143

"... por defecto == se comporta como se describe anteriormente para los tipos de referencia predefinidos y definidos por el usuario".

El tipo T no es necesariamente un tipo de referencia, por lo que el compilador no puede hacer esa suposición.

Sin embargo, esto se compilará porque es más explícito:

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

Siga a la pregunta adicional: "Pero, en caso de que esté usando un tipo de referencia, ¿el operador == usaría la comparación de referencia predefinida, o usaría la versión sobrecargada del operador si un tipo definiera uno?"

Pensé que == en los genéricos usaría la versión sobrecargada, pero la siguiente prueba demuestra lo contrario. Interesante ... ¡Me encantaría saber por qué! Si alguien sabe por favor comparte.

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

Salida

En línea: sobrecargado == llamado

Genérico:

Pulse cualquier tecla para continuar . . .

Seguimiento 2

Quiero señalar que cambiar mi método de comparación a

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

hace que se llame al operador == sobrecargado. Supongo que sin especificar el tipo (como un lugar ), el compilador no puede inferir que debería usar el operador sobrecargado ... aunque creo que tendría suficiente información para tomar esa decisión, incluso sin especificar el tipo.

Giovanni Galbo
fuente
Gracias. No me di cuenta de que la declaración era solo sobre tipos de referencia.
Hosam Aly
44
Re: Seguimiento 2: En realidad, el compilador lo vinculará con el mejor método que encuentre, que es en este caso Test.op_Equal. Pero si tenía una clase que se deriva de Test y anula al operador, entonces se llamará al operador de Test.
Hosam Aly
44
Una buena práctica que me gustaría señalar es que siempre debe hacer la comparación real dentro de un Equalsmétodo anulado (no en el ==operador).
jpbochi
11
La resolución de sobrecarga ocurre en tiempo de compilación. Entonces, cuando tenemos ==entre tipos genéricos Ty T, se encuentra la mejor sobrecarga, dadas las restricciones que conlleva T(hay una regla especial que nunca incluirá un tipo de valor para esto (lo que daría un resultado sin sentido), por lo tanto, debe haber alguna restricción que garantiza que es un tipo de referencia). En su Seguimiento 2 , si ingresa con DerivedTestobjetos y se DerivedTestderiva de Testuna nueva sobrecarga ==, pero presenta una nueva sobrecarga , volverá a tener el "problema". La sobrecarga que se llama se "quema" en la IL en tiempo de compilación.
Jeppe Stig Nielsen
1
curiosamente, esto parece funcionar para los tipos de referencia generales (donde cabría esperar que esta comparación sea sobre la igualdad de referencia) pero para las cadenas también parece usar la igualdad de referencia, por lo que puede terminar comparando 2 cadenas idénticas y tener == (cuando está en un método genérico con la restricción de clase) dicen que son diferentes.
JonnyRaa
292

Como han dicho otros, solo funcionará cuando T esté limitado a ser un tipo de referencia. Sin restricciones, puede comparar con nulo, pero solo nulo, y esa comparación siempre será falsa para los tipos de valores no anulables.

En lugar de llamar a Equals, es mejor usar un IComparer<T>, y si no tiene más información, EqualityComparer<T>.Defaultes una buena opción:

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

Aparte de cualquier otra cosa, esto evita el boxeo / lanzamiento.

Jon Skeet
fuente
Gracias. Estaba tratando de escribir una clase de contenedor simple, por lo que solo quería delegar la operación al miembro envuelto real. Pero conocer EqualityComparer <T> .Predeterminado ciertamente me agregó valor. :)
Hosam Aly
Menor aparte, Jon; es posible que desee tener en cuenta el comentario re pobox vs yoda en mi publicación.
Marc Gravell
44
Buen consejo sobre el uso de EqualityComparer <T>
chakrit
1
+1 por señalar que se puede comparar con nulo y para el tipo de valor no anulable siempre será falso
dijo Jalal el
@BlueRaja: Sí, porque hay reglas especiales para las comparaciones con el literal nulo. Por lo tanto, "sin restricciones, puede comparar con nulo, pero solo nulo". Ya está en la respuesta. Entonces, ¿por qué exactamente esto no puede ser correcto?
Jon Skeet
41

En general, EqualityComparer<T>.Default.Equalsdebe hacer el trabajo con cualquier cosa que implemente IEquatable<T>, o que tenga una Equalsimplementación sensata .

Si, sin embargo, ==y Equalsse implementan de forma diferente por alguna razón, entonces mi trabajo sobre operadores genéricos debe ser útil; admite las versiones de operador de (entre otros):

  • Igual (valor T1, valor T2)
  • NotEqual (T valor1, T valor2)
  • GreaterThan (valor T1, valor T2)
  • Menos de (valor T1, valor T2)
  • GreaterThanOrEqual (valor T1, valor T2)
  • LessThanOrEqual (valor T1, valor T2)
Marc Gravell
fuente
Biblioteca muy interesante! :) (Nota al margen: ¿Puedo sugerirle que use el enlace a www.yoda.arachsys.com, porque el servidor de seguridad de mi lugar de trabajo bloqueó el buzón de correo? Es posible que otros puedan enfrentar el mismo problema.)
Hosam Aly
La idea es que pobox.com/~skeet será siempre apuntar a mi sitio web - incluso si se mueve en otro lugar. Tiendo a publicar enlaces a través de pobox.com en aras de la posteridad, pero actualmente puedes sustituir yoda.arachsys.com.
Jon Skeet
El problema con pobox.com es que es un servicio de correo electrónico basado en la web (o eso dice el firewall de la compañía), por lo que está bloqueado. Por eso no pude seguir su enlace.
Hosam Aly
"Sin embargo, si == y Equals se implementan de manera diferente por alguna razón" - ¡Santo cielo! ¡Qué sin embargo! Tal vez solo necesito ver un caso de uso que indique lo contrario, pero una biblioteca con semántica igual divergente probablemente tendrá mayores problemas que problemas con los genéricos.
Edward Brey
@ EdwardBrey, no te equivocas; Sería bueno si el compilador pudiera hacer cumplir eso, pero ...
Marc Gravell
31

Tantas respuestas, y ninguna explica el ¿POR QUÉ? (que Giovanni preguntó explícitamente) ...

Los genéricos .NET no actúan como plantillas de C ++. En las plantillas de C ++, la resolución de sobrecarga ocurre después de que se conocen los parámetros reales de la plantilla.

En los genéricos .NET (incluido C #), la resolución de sobrecarga ocurre sin conocer los parámetros genéricos reales. La única información que el compilador puede usar para elegir la función a llamar proviene de restricciones de tipo en los parámetros genéricos.

Ben Voigt
fuente
2
pero ¿por qué el compilador no puede tratarlos como un objeto genérico? después de todo ==funciona para todos los tipos, ya sean tipos de referencia o tipos de valores. Esa debería ser la pregunta a la que no creo que hayas respondido.
nawfal
44
@nawfal: En realidad no, ==no funciona para todos los tipos de valor. Más importante aún, no tiene el mismo significado para todos los tipos, por lo que el compilador no sabe qué hacer con él.
Ben Voigt
1
Ben, oh sí, me perdí las estructuras personalizadas que podemos crear sin ninguna ==. Se puede incluir también esa parte de su respuesta ya que supongo que ese es el punto principal aquí
nawfal
12

La compilación no puede saber que T no podría ser una estructura (tipo de valor). Entonces, debes decir que solo puede ser del tipo de referencia, creo:

bool Compare<T>(T x, T y) where T : class { return x == y; }

Esto se debe a que si T podría ser un tipo de valor, podría haber casos en los x == yque estaría mal formado, en los casos en que un tipo no tiene un operador == definido. Lo mismo sucederá para esto, que es más obvio:

void CallFoo<T>(T x) { x.foo(); }

Eso también falla, porque podrías pasar un tipo T que no tendría una función foo. C # lo obliga a asegurarse de que todos los tipos posibles siempre tengan una función foo. Eso se hace con la cláusula where.

Johannes Schaub - litb
fuente
1
Gracias por la aclaración. No sabía que los tipos de valor no admitían el operador == fuera de la caja.
Hosam Aly
1
Hosam, probé con gmcs (mono), y siempre compara referencias. (es decir, no utiliza un operador opcionalmente definido == para T)
Johannes Schaub - litb
Hay una advertencia con esta solución: el operador == no se puede sobrecargar; vea esta pregunta de StackOverflow .
Dimitri C.
8

Parece que sin la restricción de clase:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

Uno debe darse cuenta de que mientras está classrestringido Equalsen el ==operador hereda Object.Equals, mientras que el de una estructura anula ValueType.Equals.

Tenga en cuenta que:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

también da el mismo error de compilación.

Hasta ahora no entiendo por qué el compilador rechaza tener una comparación de operador de igualdad de tipo de valor. Sin embargo, sí sé que esto funciona:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Jon Limjap
fuente
sabes que soy un novato total de C #. pero creo que falla porque el compilador no sabe qué hacer. como T aún no se conoce, lo que se hace depende del tipo T si se permitirían tipos de valor. para referencias, las referencias se comparan independientemente de T. si lo hace. Igual, entonces. Igual solo se llama.
Johannes Schaub - litb
pero si lo hace == en un tipo de valor, el tipo de valor no tiene que implementar necesariamente ese operador.
Johannes Schaub - litb
Eso tendría sentido, litb :) Es posible que las estructuras definidas por el usuario no sobrecarguen ==, por lo tanto, el compilador falla.
Jon Limjap
2
El primer método de comparación no usa, Object.Equalssino que prueba la igualdad de referencia. Por ejemplo, Compare("0", 0.ToString())devolvería falso, ya que los argumentos serían referencias a cadenas distintas, las cuales tienen un cero como único carácter.
supercat
1
Menor problema con el último: no lo ha restringido a estructuras, por lo que NullReferenceExceptionpodría suceder.
Flynn1179
6

Bueno, en mi caso, quería probar unitariamente el operador de igualdad. Necesitaba llamar al código bajo los operadores de igualdad sin establecer explícitamente el tipo genérico. Los consejos para EqualityComparerno fueron útiles como método EqualityComparerllamado Equals, pero no el operador de igualdad.

Así es como he conseguido que esto funcione con tipos genéricos mediante la construcción de un LINQ. Llama al código correcto para ==y !=operadores:

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}
U. Bulle
fuente
4

Hay una entrada de MSDN Connect para esto aquí

La respuesta de Alex Turner comienza con:

Desafortunadamente, este comportamiento es por diseño y no existe una solución fácil para permitir el uso de == con parámetros de tipo que pueden contener tipos de valor.

Recep
fuente
4

Si desea asegurarse de que se llame a los operadores de su tipo personalizado, puede hacerlo mediante reflexión. Simplemente obtenga el tipo usando su parámetro genérico y recupere el MethodInfo para el operador deseado (por ejemplo, op_Equality, op_Inequality, op_LessThan ...).

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

Luego ejecute el operador utilizando el método de invocación MethodInfo y pase los objetos como parámetros.

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

Esto invocará a su operador sobrecargado y no al definido por las restricciones aplicadas en el parámetro genérico. Puede que no sea práctico, pero podría ser útil para las pruebas unitarias de sus operadores cuando se utiliza una clase base genérica que contiene un par de pruebas.

Christophe
fuente
3

Escribí la siguiente función mirando el último msdn. Puede comparar fácilmente dos objetos xy y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Charlie
fuente
44
Puede deshacerse de sus booleanos y escribirreturn ((IComparable)(x)).CompareTo(y) <= 0;
aloisdg se muda a codidact.com el
1

bool Compare(T x, T y) where T : class { return x == y; }

Lo anterior funcionará porque == se atiende en caso de tipos de referencia definidos por el usuario.
En el caso de los tipos de valor, == se puede anular. En cuyo caso, "! =" También debe definirse.

Creo que esa podría ser la razón, no permite la comparación genérica con "==".

shahkalpesh
fuente
2
Gracias. Creo que los tipos de referencia también pueden anular al operador también. Pero la razón del fracaso ahora está clara.
Hosam Aly
1
El ==token se usa para dos operadores diferentes. Si para los tipos de operandos dados existe una sobrecarga compatible del operador de igualdad, se utilizará esa sobrecarga. De lo contrario, si ambos operandos son tipos de referencia compatibles entre sí, se utilizará una comparación de referencia. Tenga en cuenta que en el Comparemétodo anterior, el compilador no puede decir que se aplica el primer significado, pero puede decir que se aplica el segundo significado, por lo que el ==token usará el último incluso si Tsobrecarga el operador de verificación de igualdad (por ejemplo, si es de tipo String) .
supercat
0

los .Equals() funciona para mí mientras que TKeyes un tipo genérico.

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}
Masoud Darvishian
fuente
Eso x.Id.Equalsno id.Equals. Presumiblemente, el compilador sabe algo sobre el tipo de x.
Hosam Aly