¿Cuándo debe anular una Clase .NET Igual ()? ¿Cuándo no debería?

La documentación del VS2005, Directrices para la sobrecarga igual a () y el operador == (Guía de progtwigción C #) indica en parte

No se recomienda el operador de anulación == en tipos no inmutables.

La documentación más nueva de .NET Framework 4, Pautas para la implementación de Equals y el operador de igualdad (==) omite esa afirmación, aunque una publicación en el contenido de la comunidad repite la afirmación y hace referencia a la documentación anterior.

Parece que es razonable anular Equals () al menos para algunas clases mutables triviales, como

public class ImaginaryNumber { public double RealPart { get; set; } public double ImaginaryPart { get; set; } } 

En matemáticas, dos números imaginarios que tienen la misma parte real y la misma parte imaginaria son de hecho iguales en el momento en que se prueba la igualdad. Es incorrecto afirmar que no son iguales , lo que sucedería si los objetos separados con el mismo RealPart e ImaginaryPart fueran Iguales () no anulados.

Por otro lado, si uno anula Equals () uno también debe anular GetHashCode (). Si un ImaginaryNumber que anula Igual () y GetHashCode () se coloca en un HashSet, y una instancia mutable cambia su valor, ese objeto ya no se encontrará en el HashSet.

¿MSDN fue incorrecto al eliminar la guía sobre no reemplazar Equals() y operator== para tipos no inmutables?

¿Es razonable anular Equals () para los tipos mutables donde la equivalencia “en el mundo real” de todas las propiedades significa que los objetos mismos son iguales (como con ImaginaryNumber )?

Si es razonable, ¿cómo se maneja mejor la mutabilidad potencial mientras una instancia de objeto participa en un HashSet u otra cosa que dependa de que GetHashCode () no cambie?

ACTUALIZAR

Me acabo de enterar de esto en MSDN

Normalmente, implementa la igualdad de valor cuando se espera que los objetos del tipo se agreguen a una colección de algún tipo, o cuando su propósito principal es almacenar un conjunto de campos o propiedades. Puede basar su definición de igualdad de valor en una comparación de todos los campos y propiedades del tipo, o puede basar la definición en un subconjunto. Pero en cualquier caso, y en ambas clases y estructuras, su implementación debe seguir las cinco garantías de equivalencia:

Me di cuenta de que quería que Equals significara dos cosas diferentes, según el contexto. Después de ponderar la entrada aquí y aquí , he decidido lo siguiente para mi situación particular:

No estoy reemplazando a Equals() y GetHashCode() , sino que prefiero preservar la convención común pero de ninguna manera omnipresente de que Equals() significa igualdad de identidad para las clases, y que Equals() significa valor de igualdad para las estructuras. El principal impulsor de esta decisión es el comportamiento de los objetos en las colecciones hash ( Dictionary , HashSet , …) si me alejo de esta convención.

Esa decisión me dejó aún sin el concepto de igualdad de valor (como se discutió en MSDN )

Cuando define una clase o estructura, usted decide si tiene sentido crear una definición personalizada de igualdad (o equivalencia) de valor para el tipo. Normalmente, implementa la igualdad de valor cuando se espera que los objetos del tipo se agreguen a una colección de algún tipo, o cuando su propósito principal es almacenar un conjunto de campos o propiedades.

Un caso típico para desear el concepto de igualdad de valores (o como lo llamo “equivalencia”) es en pruebas unitarias.

Dado

 public class A { int P1 { get; set; } int P2 { get; set; } } [TestMethod()] public void ATest() { A expected = new A() {42, 99}; A actual = SomeMethodThatReturnsAnA(); Assert.AreEqual(expected, actual); } 

la prueba fallará porque Equals() está probando la igualdad de referencia.

La prueba unitaria ciertamente podría modificarse para probar cada propiedad individualmente, pero eso traslada el concepto de equivalencia fuera de la clase al código de prueba para la clase.

Para mantener ese conocimiento encapsulado en la clase y proporcionar un marco coherente para evaluar la equivalencia, definí una interfaz que mis objetos implementan

 public interface IEquivalence { bool IsEquivalentTo(T other); } 

la implementación generalmente sigue este patrón:

 public bool IsEquivalentTo(A other) { if (object.ReferenceEquals(this, other)) return true; if (other == null) return false; bool baseEquivalent = base.IsEquivalentTo((SBase)other); return (baseEquivalent && this.P1 == other.P1 && this.P2 == other.P2); } 

Ciertamente, si tuviera suficientes clases con suficientes propiedades, podría escribir una ayuda que construya un árbol de expresiones a través de la reflexión para implementar IsEquivalentTo() .

Finalmente, implementé un método de extensión que prueba la equivalencia de dos IEnumerable :

 static public bool IsEquivalentTo (this IEnumerable first, IEnumerable second) 

Si T implementa IEquivalence esa interfaz se usa, de lo contrario Equals() se usa para comparar elementos de la secuencia. Permitir el repliegue a Equals() permite funcionar, por ejemplo, con ObservableCollection además de mis objetos comerciales.

Ahora, la afirmación en mi prueba de unidad es

 Assert.IsTrue(expected.IsEquivalentTo(actual)); 

La documentación de MSDN sobre no sobrecargar == para tipos mutables es incorrecta. No hay absolutamente nada de malo en que los tipos mutables implementen la semántica de igualdad. Dos elementos pueden ser iguales ahora, incluso si cambiarán en el futuro.

Los peligros en torno a los tipos mutables y la igualdad generalmente aparecen cuando se usan como clave en una tabla hash o permiten que los miembros mutables participen en la función GetHashCode .

Consulte las Pautas y reglas para GetHashCode por Eric Lippert .

Regla: el número entero devuelto por GetHashCode nunca debe cambiar mientras el objeto está contenido en una estructura de datos que depende del código hash que se mantiene estable

Es permisible, aunque peligroso, crear un objeto cuyo valor de código hash pueda mutar a medida que los campos del objeto muten.

No entiendo sus preocupaciones sobre GetHashCode con respecto a HashSet . GetHashCode simplemente devuelve un número que ayuda a HashSet almacenar y buscar valores internamente. Si el código hash para un objeto cambia, el objeto no se elimina de HashSet , simplemente no se almacenará en la posición más óptima.

EDITAR

Gracias a @Erik JI veo el punto.

HashSet es una colección de rendimiento y para lograr ese rendimiento depende completamente de que GetHashCode sea ​​constante durante toda la vida de la colección. Si quieres este rendimiento, entonces debes seguir estas reglas. Si no puede , tendrá que cambiar a otra cosa, como List