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

El comstackdor 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 pensaron que era necesario y por qué el comstackdor no puede realizar una implementación razonable para ninguno de los operadores cuando solo el otro está presente. Por ejemplo, Lua te permite definir solo el operador de igualdad y obtienes el otro gratis. C # podría hacer lo mismo pidiéndole que defina == o ambos == y! = Y luego compile automáticamente el operador! = Ausente como !(left == right) .

Entiendo que existen casos de esquina extraños donde algunas entidades no pueden ser iguales ni desiguales (como IEEE-754 NaN), pero esas parecen ser la excepción, no la regla. Así que esto no explica por qué los diseñadores del comstackdor de C # hicieron la excepción la regla.

He visto casos de mano de obra deficiente donde 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 && cambió a || (entiendes el punto … básicamente! (a == b) ampliado a través de las reglas de De Morgan). Esa es una práctica pobre que el comstackdor 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 maneras no naturales. Lua le permite definir solo <y = 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 hay razones válidas para permitir que el progtwigdor implemente comprobaciones de igualdad y desigualdad como prefiera. Algunas de las respuestas apuntan a casos en los que eso puede ser agradable.

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

También está en marcado contraste con las opciones de diseño para interfaces .NET como Object.Equals , Object.Equals , IEquatable.Equals donde la falta de una contraparte de NotEquals muestra que el framework considera que !Equals() objetos son desiguales y eso es todo. Además, las clases como Dictionary y métodos como .Contains() dependen exclusivamente de las interfaces mencionadas anteriormente y no usan los operadores directamente aunque estén definidos. De hecho, cuando ReSharper genera miembros con igualdad, define ambos == y != En términos de Equals() e incluso entonces solo si el usuario elige generar operadores en absoluto. Los operadores de igualdad no son necesarios en el marco para comprender la igualdad de objetos.

Básicamente, el framework .NET no se preocupa por estos operadores, solo se preocupa por unos pocos métodos Equals . La decisión de exigir que los operadores == y! = Sean definidos en tándem por el usuario se relaciona puramente con el diseño del lenguaje y no con la semántica de los objetos en lo que se refiere a .NET.

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

Al observar este código básico F #, puede comstackrlo en una biblioteca funcional. Este es el 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. Crea un comparador de igualdad solo en == y comprueba si los valores internos de la clase son iguales.

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

El estándar C # EMCA tiene un conjunto de reglas (sección 14.9) que explican cómo determinar qué operador usar al evaluar la igualdad. Para decirlo de forma simplificada y, por lo tanto, no perfectamente precisa, 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 de extrañar, entonces, que si solo uno de los operadores está presente, utilizará el operador de igualdad de referencia predeterminado, que todos los objetos tienen, 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 comstackdor 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 fue pensado de esta manera, especialmente con respecto al hecho de que todos los objetos tienen un operador de igualdad predeterminado.

Entonces, ¿por qué el comstackdor no crea automágicamente el operador != ? No puedo estar seguro a menos que alguien de Microsoft confirme esto, pero esto es lo que puedo determinar al razonar sobre los hechos.


Para evitar un comportamiento inesperado

Tal vez quiero hacer una comparación de valores en == para probar la igualdad. Sin embargo, cuando se trataba de != No me importaba si los valores eran iguales a menos que la referencia fuera igual, porque para que mi progtwig los considere iguales, solo me importa si las referencias coinciden. Después de todo, esto se describe como el comportamiento predeterminado de C # (si ambos operadores no estaban sobrecargados, como sería el caso de algunas bibliotecas .net escritas en otro idioma). Si el comstackdor agrega código automáticamente, ya no podría confiar en que el comstackdor muestre el código que debería ser compatible. El comstackdor 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 de la CLI.

En términos de que te fuerce a sobrecargarlo, en lugar de ir 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 mucho comportamiento de C ++. Vea a continuación para obtener 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 devuelve algo distinto de bool, el comstackdor no puede inferir automáticamente un tipo opuesto. Además, en el caso de que su operador devuelva bool, simplemente no tiene sentido para ellos crear código de generación que solo existiría en ese caso específico o, como dije antes, código que oculta el comportamiento predeterminado del CLR.


C # toma mucho de C ++ 3

Cuando se introdujo C #, hubo un artículo en la revista MSDN que escribió, hablando de C #:

Muchos desarrolladores desean que haya un lenguaje que sea fácil de escribir, leer y mantener como Visual Basic, pero que aún así proporcionó el poder y la flexibilidad de C ++.

Sí, el objective de diseño para C # era proporcionar casi la misma cantidad de poder que C ++, sacrificando solo un poco las conveniencias como la seguridad de tipo rígida y la recolección de basura. C # fue fuertemente modelado después de C ++.

Puede que no se sorprenda al saber que en C ++, los operadores de igualdad no tienen que devolver bool , como se muestra en este progtwig de ejemplo

Ahora, C ++ no requiere directamente que sobrecargue el operador complementario. Si compiló el código en el progtwig de ejemplo, verá que se ejecuta sin errores. Sin embargo, si trataste de agregar la línea:

 cout < < (a != b); 

conseguirás

error comstackdor C2678 (MSVC): binario '! =': no ​​se ha encontrado ningún operador que tome un operando de la izquierda del tipo 'Prueba' (o no hay una conversión aceptable) `.

Entonces, aunque C ++ en sí mismo 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.


1. Como nota al margen, el estándar C # aún requiere que sobrecargue el par de operadores si desea sobrecargar uno u otro. Esto es parte del estándar y no simplemente el comstackdor . Sin embargo, las mismas reglas con respecto a la determinación de qué operador llamar se aplican cuando está accediendo 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 si alguien necesita implementar una lógica de tres valores (es decir, null ). En casos como ese, el estándar ANSI SQL, por ejemplo, los operadores no pueden simplemente ser negados dependiendo de la entrada.

Podría tener un caso donde:

 var a = SomeObject(); 

Y a == true devuelve false y a == false también devuelve false .

Aparte de que C # difiere a C ++ en muchas áreas, la mejor explicación que puedo pensar es que en algunos casos es posible que desee tomar un enfoque ligeramente diferente para demostrar “no igualdad” que para probar “igualdad”.

Obviamente, con la comparación de cadenas, por ejemplo, puede probar la igualdad y return al bucle cuando vea caracteres no coincidentes. Sin embargo, podría no ser 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 podría aplicarse la misma técnica de return , el código podría no ser tan bonito.

Si observa implementaciones de sobrecargas de == y! = En la fuente .net, a menudo no 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 comstackdor si lo hizo implícitamente) deberían implementar! = As

 return !(d1==d2); 

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

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 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étodo, sería ilógico suponer que anular un método también daría como resultado un cambio de comportamiento a un método antónimo. Si creaste una clase con métodos Increment () y Decrement (), y anulaste Increment () en una clase secundaria, ¿esperarías que Decrement () también se anulara con el opuesto al comportamiento anulado? El comstackdor 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, funcionan conceptualmente por parejas; == y! =, , y < = y> =. Sería ilógico en este caso, desde el punto de vista del consumidor, pensar que! = Funcionó de manera diferente que ==. Por lo tanto, no se puede hacer que el comstackdor suponga que a! = B ==! (A == b) en todos los casos, pero generalmente se espera que == y! = Funcionen de manera similar, por lo que las fuerzas del comstackdor a implementar por parejas, sin embargo, en realidad terminas haciéndolo. 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 plantearse es por qué y < = y> = son pares para operadores comparativos que deben implementarse al mismo tiempo, cuando están en términos numéricos. (A = b y! (A> b) == a < = b. Debería tener que implementar los cuatro si anula uno, y probablemente se le exija que anule también == (y! =), Porque (a <= b) == (a == b) si a es semánticamente igual a b.

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

También los creadores del lenguaje probablemente lo querían de esta manera para permitir la mayor flexibilidad para los codificadores, y también para que no se hagan suposiciones sobre lo que pretendes hacer.

Esto es lo que viene primero a mi mente:

  • ¿Qué pasa si probar la desigualdad es mucho más rápido que probar la igualdad?
  • ¿Qué pasa si en algunos casos desea devolver false tanto para == y != (Es decir, si no se pueden comparar por alguna razón)

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

Como resultado:

Respondiendo que es así porque lo diseñaron para serlo, es verdad … pero no responde la pregunta “por qué” de tu pregunta.

Responder que a veces puede ser útil anular ambos de forma independiente, es cierto … pero no responde a la parte “obligatoria” 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 lenguaje debería permitirle anular solo == , y proporcionarle una implementación predeterminada de != es ! ese. Si por casualidad quieres anular != , Hazlo.

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

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

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

Solo para agregar a las excelentes respuestas aquí:

¡Considere lo que pasaría en el depurador , cuando intente entrar en un operador != Y termine en un operador == lugar! ¡Habla de confuso!

Tiene sentido que CLR le permita liberarse de uno u otro operador, ya que debe funcionar en muchos idiomas. Pero hay muchos ejemplos de C # que no expone las características de CLR ( ref returns y locals, por ejemplo), y muchos ejemplos de implementación de características que no están en el CLR en sí (p. Ej .: using , lock , foreach , etc.).

Los lenguajes de progtwigción son reordenamientos sintácticos de una statement lógica excepcionalmente compleja. 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 definida para que el lenguaje determine la igualdad de objetos. Por ejemplo, la comparación NULL! = NULL puede arrojar una llave inglesa en la definición de un sistema de igualdad que no implementa una statement de no igualdad.

Ahora, con respecto 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 especificaciones del lenguaje C # decidiera que el progtwigdor se viera obligado a definir explícitamente la igualdad y la no igualdad de un objeto en conjunto.

En resumen, consistencia forzada.

‘==’ y ‘! =’ son siempre verdaderos opuestos, sin importar cómo los definas, definidos como tales por su definición verbal de “iguales” y “no iguales”. Al definir solo uno de ellos, se abre a una inconsistencia de operador de igualdad donde tanto ‘==’ como ‘! =’ Pueden ser verdaderos o ambos ser falsos para dos valores dados. Debe definir ambos ya que cuando elige definir uno, también debe definir el otro adecuadamente para que quede descaradamente claro cuál es su definición de “igualdad”. La otra solución para el comstackdor es solo permitirle anular ‘==’ OR ‘! =’ Y dejar al otro como negación inherente del otro. Obviamente, ese no es el caso con el comstackdor de C # y estoy seguro de que hay una razón válida para eso que puede atribuirse estrictamente como una opción de simplicidad.

La pregunta que debe hacerse es “¿por qué tengo que anular a los operadores?” Esa es una decisión fuerte que requiere un razonamiento fuerte. Para objetos, ‘==’ y ‘! =’ Compare por referencia. Si debe sobrescribirlos para NO comparar por referencia, está creando una incoherencia general del operador que no es evidente para ningún otro desarrollador que examine ese código. Si intentas hacer la pregunta “¿es el estado de estas dos instancias equivalente?”, Entonces debes implementar IEquatible, definir Equals () y utilizar esa llamada a método.

Por último, IEquatable () no define NotEquals () por el mismo razonamiento: posibilidad de abrir incoherencias 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 se está forzando la cuestión de la coherencia en la determinación de la igualdad.

Editar: Este es simplemente mi razonamiento.

Probablemente solo algo en lo que no pensaron no tuvo tiempo de hacerlo.

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

Tienes razón, con una pequeña cantidad de trabajo, el comstackdor podría darnos esto gratis.