¿Por qué debemos definir ambos == y! = En C #?

346

El compilador de C # requiere que siempre que un tipo personalizado defina operador ==, también debe definir !=(ver aquí ).

¿Por qué?

Tengo curiosidad por saber por qué los diseñadores lo consideraron necesario y por qué el compilador no puede usar una implementación razonable para cualquiera de los operadores cuando solo está presente el otro. Por ejemplo, Lua le permite definir solo el operador de igualdad y usted obtiene el otro de forma gratuita. C # podría hacer lo mismo pidiéndole que defina == o ambos == y! = Y luego compile automáticamente el operador que falta! = Como !(left == right).

Entiendo que hay casos extraños en los que algunas entidades pueden no ser iguales ni desiguales (como IEEE-754 NaN's), pero esas parecen ser 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 mano de obra pobre donde se define el operador de igualdad, luego el operador de desigualdad es una copia y pega con todas y cada una de las comparaciones invertidas y cada && cambiado a || (entiendes el punto ... ¡básicamente! (a == b) expandido 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 vale para los operadores <> <=> =. No puedo imaginar casos en los que deba definirlos de forma poco natural. Lua le permite definir solo <y <= y define> = y> naturalmente a través de la negación de los formadores. ¿Por qué C # no hace lo mismo (al menos 'por defecto')?

EDITAR

Aparentemente, existen razones válidas para permitir que el programador implemente verificaciones de igualdad y desigualdad como quiera. Algunas de las respuestas apuntan a casos en los que eso puede ser bueno.

El núcleo de mi pregunta, sin embargo, es ¿por qué esto se requiere a la fuerza en C # cuando generalmente no es lógicamente necesario?

También contrasta notablemente con las opciones de diseño para interfaces .NET como Object.Equals, IEquatable.Equals IEqualityComparer.Equalsdonde la falta de una NotEqualscontraparte muestra que el marco considera los !Equals()objetos desiguales y eso es todo. Además, las clases como Dictionaryy los métodos como .Contains()dependen exclusivamente de las interfaces mencionadas anteriormente 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, el marco .NET no se preocupa por estos operadores, solo se preocupa por algunos Equalsmétodos. La decisión de exigir que los operadores == y! = Sean definidos en conjunto por el usuario está relacionada exclusivamente con el diseño del lenguaje y no con la semántica de objetos en lo que respecta a .NET.

Stefan Dragnev
fuente
24
+1 por una excelente pregunta. No parece haber ninguna razón en absoluto ... por supuesto, el compilador podría suponer que a! = B se puede traducir a! (A == b) a menos que se indique lo contrario. Tengo curiosidad por saber por qué decidieron hacer esto también.
Patrick87
16
Esto es lo más rápido que he visto una pregunta votada y favorecida.
Christopher Currens
26
Estoy seguro de que @Eric Lippert también puede proporcionar una respuesta sólida.
Yuck
17
"El científico no es una persona que da las respuestas correctas, es uno que hace las preguntas correctas". Es por eso que en SE se alientan las preguntas. ¡Y funciona!
Adriano Carneiro
44
La misma pregunta para Python: stackoverflow.com/questions/4969629/…
Josh Lee

Respuestas:

161

No puedo hablar por los diseñadores de idiomas, pero por lo que puedo razonar, parece que fue una decisión de diseño intencional y adecuada.

Mirando este código básico de F #, puede compilarlo en una biblioteca de trabajo. Este es un código legal para F #, y solo sobrecarga el operador de igualdad, no la 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. Solo crea un comparador de igualdad ==y comprueba si los valores internos de la clase son iguales.

Si bien no puede crear una clase como esta en C #, puede usar una que se compiló para .NET. Es obvio que usará nuestro operador sobrecargado. == Entonces, ¿ para qué sirve el tiempo de ejecución !=?

El estándar C # EMCA tiene un montón de reglas (sección 14.9) que explican cómo determinar qué operador usar al evaluar la igualdad. Para ponerlo demasiado simplificado y, por lo tanto, no es perfectamente preciso, 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 es sorprendente, entonces, que si solo uno de los operadores está presente, usará el operador de igualdad de referencia predeterminado, que tienen todos los objetos, no hay una sobrecarga para él. 1

Sabiendo que este es el caso, la verdadera pregunta es: ¿por qué se diseñó de esta manera y por qué el compilador no se da cuenta por sí mismo? Mucha gente dice que esto no fue una decisión de diseño, pero me gusta pensar que se pensó de esta manera, especialmente con respecto 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 confirme esto, pero esto es lo que puedo determinar a partir del razonamiento sobre los hechos.


Para evitar comportamientos inesperados

Tal vez quiero 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 se describe realmente como el 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 podría confiar en el compilador para generar el código que debería cumplir. El compilador no debe escribir código oculto que cambie el comportamiento del suyo, especialmente cuando el código que ha escrito está dentro de los estándares de C # y la CLI.

En términos de obligarlo a sobrecargarlo, en lugar de pasar al comportamiento predeterminado, solo puedo decir firmemente que está en el estándar (EMCA-334 17.9.2) 2 . El estándar no especifica por qué. Creo que esto se debe al hecho de que C # toma prestado mucho comportamiento de C ++. Vea a continuación más información sobre esto.


Cuando anula !=y ==, no tiene que devolver bool.

Esta 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 está devolviendo algo que no sea bool, el compilador no puede inferir automáticamente un tipo opuesto. Además, en el caso de que el operador hace return bool, simplemente no tiene sentido para ellos crean generar código que sólo existiría en ese caso en particular uno o, como he dicho anteriormente, el código que oculta el comportamiento predeterminado del CLR.


C # toma prestado mucho de C ++ 3

Cuando se introdujo C #, había un artículo en la revista MSDN que escribió, 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 así proporcionara el poder y la flexibilidad de C ++.

Sí, el objetivo de diseño para C # era dar casi la misma cantidad de energía que C ++, sacrificando solo un poco por comodidades como la seguridad de tipo rígido y la recolección de basura. C # fue fuertemente modelado después de C ++.

Puede que 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 sobrecargue 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 izquierdo del tipo 'Prueba' (o no hay una conversión aceptable) '.

Entonces, si bien C ++ en sí no requiere que se sobrecargue en pares, no le permitirá usar un operador de igualdad que no haya 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 los dos. Esta es una parte del estándar y no simplemente el compilador . Sin embargo, las mismas reglas con respecto a la determinación de a qué operador llamar se aplican 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í

Christopher Currens
fuente
75
" no tienes que volver bool " .. Creo que esa es nuestra respuesta allí mismo; bien hecho. Si! (O1 == o2) ni siquiera está bien definido, entonces no puede esperar que el estándar dependa de él.
Brian Gordon el
10
Irónicamente, en un tema sobre operadores lógicos, ha utilizado un doble negativo en la oración, "C # no le permite no anular solo uno de los dos", lo que en realidad significa lógicamente que "C # le permite anular solo uno de los dos".
Dan Diplo
13
@Dan Diplo, está bien, no es que no esté prohibido no hacer eso o no.
Christopher Currens
66
Si es cierto, sospecho que el número 2 es correcto; pero ahora la pregunta es, ¿por qué permitir ==devolver cualquier cosa que no sea a bool? Si devuelve un int, ya no puede usarlo como una declaración condicional, por ejemplo. if(a == b)ya no se compilará. ¿Cuál es el punto de?
BlueRaja - Danny Pflughoeft
2
Podría devolver un objeto que tenga una conversión implícita a bool (operador verdadero / falso) pero todavía no puedo ver dónde sería útil.
Stefan Dragnev
53

Probablemente si alguien necesita implementar una lógica de tres valores (es decir null). En casos como ese, por ejemplo, SQL estándar ANSI, los operadores no pueden simplemente negarse según la entrada.

Podría tener un caso donde:

var a = SomeObject();

Y a == truevuelve falsey a == falsetambién vuelve false.

Yuck
fuente
1
Yo diría que en ese caso, dado aque no es un booleano, deberías compararlo con una enumeración de tres estados, no real trueo false.
Brian Gordon el
18
Por cierto, no puedo creer que nadie haya hecho referencia a la broma FileNotFound ..
Brian Gordon
28
Si a == truees falseasí, siempre esperaría a != trueserlo true, a pesar de a == falseserfalse
Fede
20
@Fede golpeó el clavo en la cabeza. Esto realmente no tiene nada que ver con la pregunta : estás explicando por qué alguien podría querer anular ==, no por qué debería verse obligado a anular ambos.
BlueRaja - Danny Pflughoeft
66
@Yuck: Sí, hay una buena implementación predeterminada: es el caso común. Incluso en su ejemplo, la implementación predeterminada de !=sería correcta. En el caso extremadamente poco común en el que quisiéramos x == yy x != yambos fueran falsos (no puedo pensar en un solo ejemplo que tenga sentido) , aún podrían anular la implementación predeterminada y proporcionar la suya propia. ¡Siempre haga que el caso común sea el predeterminado!
BlueRaja - Danny Pflughoeft
25

Aparte de que C # difiere de C ++ en muchas áreas, la mejor explicación que puedo pensar 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, puede probar la igualdad y returnsalir del bucle cuando vea caracteres que no coinciden. Sin embargo, puede que no sea tan limpio con problemas más complicados. El filtro de floración viene a la mente; 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 returnpodría aplicarse la misma técnica, el código podría no ser tan bonito.

Brian Gordon
fuente
66
Gran ejemplo; Creo que entiendes por qué. Un simple lo !encierra en una única definición de igualdad, donde un codificador informado podría optimizar ambos == y !=.
Yuck
13
Esto explica por qué se les debe dar la opción de anular ambos, no por qué se les debe obligar a hacerlo.
BlueRaja - Danny Pflughoeft
1
En ese sentido, no tienen que optimizar ambos, solo tienen que proporcionar una definición clara de ambos.
Jesper
22

Si observa implementaciones de sobrecargas de == y! = En la fuente .net, a menudo no se implementan! = As! (Left == right). 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 hiciera implícitamente) implementara! = As

return !(d1==d2);

entonces está asumiendo la implementación interna de == y! = en las cosas a las que hace referencia su clase. Evitar esa suposición puede ser la filosofía detrás de su decisión.

hacha - hecho con SOverflow
fuente
17

Para responder a su edición, con respecto a por qué se ve obligado a anular ambos si anula uno, todo está en la herencia.

Si anula ==, lo más probable es que proporcione algún tipo de igualdad semántica o estructural (por ejemplo, DateTimes es igual si sus propiedades de InternalTicks son iguales aunque puedan ser instancias diferentes), entonces está cambiando el comportamiento predeterminado del operador de Object, que es el padre de todos los objetos .NET. El operador == es, en C #, un método, cuya implementación base Object.operator (==) realiza una comparación referencial. Object.operator (! =) Es otro método diferente, que también realiza una comparación referencial.

En casi cualquier otro caso de anulación de métodos, sería ilógico suponer que anular un método también daría lugar a un cambio de comportamiento a un método antónimo. Si crea una clase con los métodos Incremento () y Decremento (), y anula el Incremento () en una clase secundaria, ¿esperaría que el Decremento () también se anule con lo contrario de su comportamiento anulado? El compilador no puede ser lo suficientemente inteligente como para generar una función inversa para cualquier implementación de un operador en todos los casos posibles.

Sin embargo, los operadores, aunque implementados de manera muy similar a los métodos, trabajan conceptualmente en parejas; == y! =, <y>, y <= y> =. Sería ilógico en este caso desde el punto de vista de un consumidor pensar que! = Funcionó de manera diferente a ==. Por lo tanto, no se puede hacer que el compilador suponga que a! = B ==! (A == b) en todos los casos, pero generalmente se espera que == y! = Operen de manera similar, por lo que el compilador obliga implementar en pares, sin embargo, en realidad terminas haciendo eso. Si, para su clase, a! = B ==! (A == b), simplemente implemente el operador! = Usando! (==), pero si esa regla no se cumple en todos los casos para su objeto (por ejemplo , si la comparación con un valor particular, igual o desigual, no es válida), entonces debe ser más inteligente que el IDE.

¡La pregunta REAL que debe hacerse es por qué <y> y <= y> = son pares para operadores comparativos que deben implementarse simultáneamente, cuando están en términos numéricos! (A <b) == a> = by! (A> b) == a <= b. Debería estar obligado a implementar los cuatro si anula uno, y probablemente también deba anular == (y! =) También, porque (a <= b) == (a == b) si a es semánticamente igual a b.

KeithS
fuente
14

Si sobrecarga == para su tipo personalizado, y no! = Entonces será manejado por el operador! = Para el objeto! = Objeto ya que todo se deriva del objeto, y esto sería muy diferente de CustomType! = CustomType.

Además, los creadores del lenguaje probablemente querían que de esta manera se permitiera la mayor flexibilidad para los codificadores, y también para que no hagan suposiciones sobre lo que pretenden hacer.

Chris Mullins
fuente
Inspiró esta pregunta: stackoverflow.com/questions/6919259/…
Brian Gordon
2
La razón de la flexibilidad también se puede aplicar a una gran cantidad de características que decidieron dejar de lado, por ejemplo blogs.msdn.com/b/ericlippert/archive/2011/06/23/…. Así no explica su elección para La implementación de operadores de igualdad.
Stefan Dragnev
Eso es muy interesante. Parece que parece que sería una característica útil. No tengo idea de por qué habrían dejado eso fuera.
Chris Mullins
11

Esto es lo que primero viene a mi mente:

  • Y si probar la desigualdad es mucho más rápido que probar la igualdad?
  • ¿Qué pasa si en algunos casos desea devolver falsetanto para ==y!= (es decir, si no se pueden comparar por alguna razón)
Dan Abramov
fuente
55
En el primer caso, podría implementar pruebas de igualdad en términos de pruebas de desigualdad.
Stefan Dragnev
El segundo caso romperá estructuras como Dictionaryy Listsi usa su tipo personalizado con ellas.
Stefan Dragnev
2
@Stefan: Dictionaryutiliza GetHashCodey Equals, no los operadores. Listusos Equals.
Dan Abramov
6

Bueno, probablemente sea solo una opción de diseño, pero como dices, x!= yno tiene que ser lo mismo !(x == y). Al no agregar una implementación predeterminada, está seguro de que no puede olvidar implementar una implementación específica. Y si es tan trivial como dices, puedes implementar uno usando el otro. No veo cómo esto es 'mala práctica'.

Puede haber algunas otras diferencias entre C # y Lua también ...

GolezTrol
fuente
1
Es una práctica excelente implementar uno en términos del otro. Lo acabo de ver hecho de otra manera, lo cual es propenso a errores.
Stefan Dragnev
3
Ese es el problema del programador, no el problema de C #. Los programadores que no implementen este derecho, también fracasarán en otras áreas.
GolezTrol
@GolezTrol: ¿Podría explicar esa referencia de Lua? No estoy familiarizado con Lua, pero su referencia parece insinuar algo.
zw324
@Ziyao Wei: OP señala que Lua hace esto de manera diferente a C # (Lua aparentemente agrega contrapartes predeterminadas para los operadores mencionados). Solo trato de trivializar tal comparación. Si no hay diferencias, entonces al menos uno de ellos no tiene derecho a existir. Debo decir que tampoco conozco a Lua.
GolezTrol
6

Las palabras clave en su pregunta son " por qué " y " debe ".

Como resultado:

Responder es así porque lo diseñaron para que sea así, es cierto ... pero no responder la parte del "por qué" de su pregunta.

Contestar que a veces puede ser útil anular ambos de forma independiente, es cierto ... pero no responder la parte "imprescindible" de su pregunta.

Creo que la respuesta simple es que no hay ninguna razón convincente por la que C # requiera que anule ambos.

El idioma debería permitirle anular solo ==y proporcionarle una implementación predeterminada de !=eso es !eso. Si también quieres anular !=, hazlo .

No fue una buena decisión. Los humanos diseñan lenguajes, los humanos no son perfectos, C # no es perfecto. Encogimiento de hombros y QED

Greg Hendershott
fuente
5

Solo para agregar a las excelentes respuestas aquí:

Considere lo que sucedería en el depurador , cuando intente ingresar a un !=operador y terminar en un== operador! Hable acerca de confundir!

Tiene sentido que CLR le permita la libertad de omitir uno u otro operador, ya que debe funcionar con muchos idiomas. Pero hay un montón de ejemplos de C # no exponer las características CLR ( refdevoluciones y locales, por ejemplo), y un montón de ejemplos de implementación de características no en la propia CLR (por ejemplo: using, lock, foreach, etc).

Andrew Russell
fuente
3

Los lenguajes de programación son reordenamientos sintácticos de enunciados lógicos excepcionalmente complejos. Con eso en mente, ¿puedes definir un caso de igualdad sin definir un caso de no igualdad? La respuesta es no. Para que un objeto a sea igual al objeto b, entonces el inverso del objeto a no es igual a b también debe ser verdadero. Otra forma de mostrar esto es

if a == b then !(a != b)

Esto proporciona la capacidad definitiva para que el lenguaje determine la igualdad de los objetos. Por ejemplo, la comparación NULL! = NULL puede arrojar una llave en la definición de un sistema de igualdad que no implementa una declaración de no igualdad.

Ahora, en lo que respecta a la idea de! = Simplemente ser una definición reemplazable como en

if !(a==b) then a!=b

No puedo discutir con eso. Sin embargo, lo más probable es que el grupo de especificación del lenguaje C # haya decidido que el programador se vea obligado a definir explícitamente la igualdad y la no igualdad de un objeto juntos

Josh
fuente
Nullsolo sería un problema porque nulles una entrada válida para ==, lo que no debería ser, aunque obviamente, es enormemente conveniente. Personalmente, creo que permite grandes cantidades de programación vaga y descuidada.
nicodemus13
2

En resumen, consistencia forzada.

'==' y '! =' son siempre verdaderos opuestos, sin importar cómo los defina, definidos como tales por su definición verbal de "igual" y "no igual". Al definir solo uno de ellos, se abre a una inconsistencia del operador de igualdad donde '==' y '! =' Pueden ser verdaderos o ambos falsos para dos valores dados. Debe definir ambos, ya que cuando elige definir uno, también debe definir el otro apropiadamente para que quede claramente claro cuál es su definición de "igualdad". La otra solución para el compilador es permitirle solo anular '==' OR '! =' Y dejar que el otro lo niegue inherentemente. Obviamente, ese no es el caso con el compilador de C # y estoy seguro de que allí '

La pregunta que debe hacerse es "¿por qué tengo que anular los operadores?" Esa es una decisión fuerte de tomar que requiere un fuerte razonamiento. Para los objetos, '==' y '! =' Comparar por referencia. Si debe anularlos para NO compararlos por referencia, está creando una inconsistencia general del operador que no es evidente para ningún otro desarrollador que lea detenidamente ese código. Si está intentando hacer la pregunta "¿es equivalente el estado de estas dos instancias?", Entonces debe implementar IEquatible, definir Equals () y utilizar esa llamada al método.

Por último, IEquatable () no define NotEquals () por el mismo razonamiento: potencial para abrir inconsistencias de operador de igualdad. NotEquals () SIEMPRE debe devolver! Equals (). Al abrir la definición de NotEquals () a la clase que implementa Equals (), una vez más está forzando el tema de la coherencia en la determinación de la igualdad.

Editar: este es simplemente mi razonamiento.

Emoire
fuente
Esto es ilógico. Si la razón fuera la coherencia, se aplicaría lo contrario: solo sería capaz de implementar operator ==y el compilador inferiría operator !=. Después de todo, esto es para lo que hace operator +y operator +=.
Konrad Rudolph
1
foo + = bar; es una asignación compuesta, abreviatura de foo = foo + bar ;. No es un operador.
blenderfreaky
Si de hecho son 'siempre opuestos verdaderos', entonces esa sería una razón para definir automáticamente a != bcomo !(a == b)... (que no es lo que hace C #).
andrewf
-3

Probablemente, algo en lo que no pensaron no tuvo tiempo de hacer.

Siempre uso tu método cuando sobrecargo ==. Entonces solo lo uso en el otro.

Tienes razón, con una pequeña cantidad de trabajo, el compilador podría darnos esto de forma gratuita.

Rhyous
fuente