¿Por qué debemos definir ambos == y! = en C#?
El compilador de C# requiere que cada vez que un tipo personalizado defina operador ==
, también debe definirlo !=
(consulte aquí ).
¿Por qué?
Tengo curiosidad por saber por qué los diseñadores lo consideraron necesario y por qué el compilador no puede establecer de forma predeterminada una implementación razonable para cualquiera de los operadores cuando solo el otro está presente. Por ejemplo, Lua te permite definir sólo el operador de igualdad y obtienes el otro gratis. C# podría hacer lo mismo pidiéndole que defina == o ambos == y != y luego compilar automáticamente el operador != que falta como !(left == right)
.
Entiendo que hay casos extraños en los que algunas entidades pueden no ser ni iguales ni desiguales (como las de IEEE-754 NaN), pero parecen la excepción, no la regla. Entonces esto no explica por qué los diseñadores del compilador de C# hicieron de la excepción la regla.
He visto casos de mala mano de obra en los que se define el operador de igualdad, luego el operador de desigualdad es un copiar y pegar con todas y cada una de las comparaciones invertidas y cada && cambiado a || (entiendes el punto... ¡básicamente! (a==b) ampliado a través de las reglas de De Morgan). Esa es una mala práctica que el compilador podría eliminar por diseño, como es el caso de Lua.
Nota: Lo mismo se aplica a los operadores < > <= >=. No puedo imaginar casos en los que sea necesario definirlos de forma antinatural. Lua te permite definir solo < y <= y define >= y > naturalmente a través de la negación de los primeros. ¿Por qué C# no hace lo mismo (al menos "de forma predeterminada")?
EDITAR
Aparentemente existen razones válidas para permitir que el programador implemente controles de igualdad y desigualdad como quiera. Algunas de las respuestas apuntan a casos en los que eso puede ser bueno.
Sin embargo, el núcleo de mi pregunta es ¿por qué esto es obligatorio en C# cuando normalmente no es lógicamente necesario?
También contrasta notablemente con las opciones de diseño para interfaces .NET como Object.Equals
, IEquatable.Equals
IEqualityComparer.Equals
donde la falta de una NotEquals
contraparte muestra que el marco considera !Equals()
los objetos como desiguales y eso es todo. Además, las clases Dictionary
y métodos like .Contains()
dependen exclusivamente de las interfaces antes mencionadas y no utilizan los operadores directamente incluso si están definidos. De hecho, cuando ReSharper genera miembros de igualdad, define ambos ==
y !=
en términos de Equals()
e incluso entonces solo si el usuario elige generar operadores. El marco no necesita los operadores de igualdad para comprender la igualdad de objetos.
Básicamente, al marco .NET no le importan estos operadores, solo le importan unos pocos Equals
métodos. La decisión de exigir que el usuario defina los operadores == y != en conjunto está relacionada puramente con el diseño del lenguaje y no con la semántica del objeto en lo que respecta a .NET.
No puedo hablar por los diseñadores del lenguaje, pero por lo que puedo razonar, parece que fue una decisión de diseño adecuada y intencional.
Al observar este código básico de F#, puede compilarlo en una biblioteca funcional. Este es un código legal para F# y solo sobrecarga el operador de igualdad, no el de desigualdad:
module Module1
type Foo() =
let mutable myInternalValue = 0
member this.Prop
with get () = myInternalValue
and set (value) = myInternalValue <- value
static member op_Equality (left : Foo, right : Foo) = left.Prop = right.Prop
//static member op_Inequality (left : Foo, right : Foo) = left.Prop <> right.Prop
Esto hace exactamente lo que parece. Crea un comparador de igualdad ==
solo y verifica si los valores internos de la clase son iguales.
Si bien no puedes crear una clase como esta en C#, puedes usar una que haya sido compilada para .NET. Es obvio que utilizará nuestro operador sobrecargado. ==
Entonces, ¿para qué se utiliza el tiempo de ejecución !=
?
El estándar C# EMCA tiene una gran cantidad de reglas (sección 14.9) que explican cómo determinar qué operador usar al evaluar la igualdad. Para decirlo demasiado simplificado y, por lo tanto, no perfectamente exacto, si los tipos que se comparan son del mismo tipo y hay un operador de igualdad sobrecargado presente, utilizará esa sobrecarga y no el operador de igualdad de referencia estándar heredado de Object. No sorprende, entonces, que si solo uno de los operadores está presente, utilizará el operador de igualdad de referencia predeterminado, que tienen todos los objetos, no hay una sobrecarga para ello. 1
Sabiendo que este es el caso, la verdadera pregunta es: ¿Por qué se diseñó de esta manera y por qué el compilador no lo descubre por sí solo? Mucha gente dice que esto no fue una decisión de diseño, pero me gusta pensar que se pensó de esta manera, especialmente en lo que respecta al hecho de que todos los objetos tienen un operador de igualdad predeterminado.
Entonces, ¿por qué el compilador no crea automáticamente el !=
operador? No puedo estar seguro a menos que alguien de Microsoft lo confirme, pero esto es lo que puedo determinar a partir del razonamiento sobre los hechos.
Para evitar comportamientos inesperados
Quizás quiera hacer una comparación de valores para ==
probar la igualdad. Sin embargo, cuando se trataba de eso, !=
no me importaba en absoluto si los valores eran iguales a menos que la referencia fuera igual, porque para que mi programa los considere iguales, solo me importa si las referencias coinciden. Después de todo, esto en realidad se describe como comportamiento predeterminado de C# (si ambos operadores no estuvieran sobrecargados, como sería el caso de algunas bibliotecas .net escritas en otro idioma). Si el compilador agregaba código automáticamente, ya no podía confiar en que el compilador generara código que debería ser compatible. El compilador no debe escribir código oculto que cambie su comportamiento, especialmente cuando el código que ha escrito cumple con los estándares de C# y CLI.
En términos de obligarlo a sobrecargarlo, en lugar de recurrir al comportamiento predeterminado, solo puedo decir firmemente que está en el estándar (EMCA-334 17.9.2) 2 . La norma no especifica por qué. Creo que esto se debe al hecho de que C# toma prestado mucho comportamiento de C++. Consulte a continuación para obtener más información sobre esto.
Cuando anula !=
y ==
, no es necesario que devuelva bool.
Ésta es otra razón probable. En C#, esta función:
public static int operator ==(MyClass a, MyClass b) { return 0; }
es tan válido como este:
public static bool operator ==(MyClass a, MyClass b) { return true; }
Si devuelve algo distinto de bool, el compilador no puede inferir automáticamente un tipo opuesto. Además, en el caso de que su operador devuelva bool, simplemente no tiene sentido que cree un código generado que solo existiría en ese caso específico o, como dije anteriormente, un código que oculte el comportamiento predeterminado de CLR.
C# toma prestado mucho de C++ 3
Cuando se introdujo C#, había un artículo en la revista MSDN que escribía hablando de C#:
Muchos desarrolladores desearían que hubiera un lenguaje que fuera fácil de escribir, leer y mantener como Visual Basic, pero que aún ofreciera la potencia y flexibilidad de C++.
Sí, el objetivo de diseño de C# era proporcionar casi la misma cantidad de potencia que C++, sacrificando sólo un poco por comodidades como la seguridad de tipos rígidos y la recolección de basura. C# fue fuertemente modelado a partir de C++.
Quizás no le sorprenda saber que en C++, los operadores de igualdad no tienen que devolver bool , como se muestra en este programa de ejemplo.
Ahora, C++ no requiere directamente que sobrecargues el operador complementario. Si compiló el código en el programa de ejemplo, verá que se ejecuta sin errores. Sin embargo, si intentaste agregar la línea:
cout << (a != b);
conseguirás
error del compilador C2678 (MSVC): binario '!=': no se encontró ningún operador que tome un operando del lado izquierdo de tipo 'Prueba' (o no hay una conversión aceptable)`.
Entonces, si bien C++ en sí no requiere que sobrecargues en pares, no te permitirá usar un operador de igualdad que no hayas sobrecargado en una clase personalizada. Es válido en .NET, porque todos los objetos tienen uno predeterminado; C++ no lo hace.
1. Como nota al margen, el estándar C# aún requiere que sobrecargue el par de operadores si desea sobrecargar cualquiera de ellos. Esto es parte del estándar y no simplemente del compilador . Sin embargo, se aplican las mismas reglas con respecto a la determinación de a qué operador llamar cuando accede a una biblioteca .net escrita en otro idioma que no tiene los mismos requisitos.
2. EMCA-334 (pdf) ( http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf )
3. Y Java, pero ese no es realmente el punto aquí.
Probablemente por si alguien necesita implementar una lógica de tres valores (es decir null
). En casos como ese (SQL estándar ANSI, por ejemplo), los operadores no pueden simplemente negarse dependiendo de la entrada.
Podría tener un caso donde:
var a = SomeObject();
Y a == true
regresa false
y a == false
también regresa false
.
Aparte de que C# difiere de C++ en muchas áreas, la mejor explicación que se me ocurre es que en algunos casos es posible que desee adoptar un enfoque ligeramente diferente para demostrar "no igualdad" que para demostrar "igualdad".
Obviamente, con la comparación de cadenas, por ejemplo, puedes simplemente probar la igualdad y return
salir del bucle cuando veas caracteres que no coinciden. Sin embargo, puede que no sea tan claro con problemas más complicados. Me viene a la mente el filtro de floración ; Es muy fácil saber rápidamente si el elemento no está en el conjunto, pero es difícil saber si el elemento está en el conjunto. Si bien se podría aplicar la misma return
técnica, es posible que el código no sea tan bonito.
Si observa las implementaciones de sobrecargas de == y != en la fuente .net, a menudo no implementan != como !(izquierda == derecha). Lo implementan completamente (como ==) con lógica negada. Por ejemplo, DateTime implementa == como
return d1.InternalTicks == d2.InternalTicks;
y != como
return d1.InternalTicks != d2.InternalTicks;
Si usted (o el compilador si lo hizo implícitamente) implementara != como
return !(d1==d2);
entonces estás haciendo una suposición sobre la implementación interna de == y != en las cosas a las que hace referencia tu clase. Evitar esa suposición puede ser la filosofía detrás de su decisión.