¿Cuál es la “mejor práctica” para comparar dos instancias de un tipo de referencia?

Me encontré con esto recientemente, hasta ahora he estado anulando con creces el operador de igualdad ( == ) y / o el método Equals para ver si dos tipos de referencias contenían realmente los mismos datos (es decir, dos instancias diferentes que tienen el mismo aspecto).

He estado usando esto aún más desde que estoy obteniendo más en las pruebas automatizadas (comparando los datos de referencia / esperados con los devueltos).

Al revisar algunas de las pautas de estándares de encoding en MSDN , encontré un artículo que desaconseja. Ahora entiendo por qué el artículo dice esto (porque no son la misma instancia ) pero no responde la pregunta:

  1. ¿Cuál es la mejor manera de comparar dos tipos de referencia?
  2. ¿Deberíamos implementar IComparable ? (También he visto mencionar que esto debería reservarse solo para tipos de valor).
  3. ¿Hay alguna interfaz que no conozca?
  4. ¿Deberíamos tirar el nuestro?

Muchas gracias ^ _ ^

Actualizar

Parece que he leído mal la documentación (ha sido un día largo) y que Igual puede ser el camino a seguir.

Si está implementando tipos de referencia, debe considerar anular el método Equals en un tipo de referencia si su tipo se parece a un tipo base como Point, String, BigNumber, etc. La mayoría de los tipos de referencia no deben sobrecargar al operador de igualdad , incluso si anula Igual . Sin embargo, si está implementando un tipo de referencia que pretende tener semántica de valores, como un tipo de número complejo, debe anular el operador de igualdad.

Parece que está codificando en C #, que tiene un método llamado Equals que su clase debería implementar, si desea comparar dos objetos usando algún otro indicador que “son estos dos punteros (porque los identificadores de objetos son solo eso, punteros) para la misma dirección de memoria? “.

Agarré un código de muestra desde aquí :

 class TwoDPoint : System.Object { public readonly int x, y; public TwoDPoint(int x, int y) //constructor { this.x = x; this.y = y; } public override bool Equals(System.Object obj) { // If parameter is null return false. if (obj == null) { return false; } // If parameter cannot be cast to Point return false. TwoDPoint p = obj as TwoDPoint; if ((System.Object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public bool Equals(TwoDPoint p) { // If parameter is null return false: if ((object)p == null) { return false; } // Return true if the fields match: return (x == px) && (y == py); } public override int GetHashCode() { return x ^ y; } } 

Java tiene mecanismos muy similares. El método equals () es parte de la clase Object , y su clase lo sobrecarga si desea este tipo de funcionalidad.

La razón por la que la sobrecarga ‘==’ puede ser una mala idea para los objetos es que, por lo general, aún desea poder hacer las comparaciones “¿son estos el mismo puntero?”. Por lo general, se confía en ellos para, por ejemplo, insertar un elemento en una lista donde no se permiten duplicados, y algunas de las funciones de su infraestructura pueden no funcionar si este operador está sobrecargado de una forma no estándar.

Implementar la igualdad en .NET correctamente, de manera eficiente y sin duplicación de código es difícil. Específicamente, para tipos de referencia con semántica de valores (es decir, tipos inmutables que tratan la igualdad como igualdad ), debe implementar la interfaz System.IEquatable y debe implementar todas las diferentes operaciones ( Equals , GetHashCode y == GetHashCode != ) .

Como ejemplo, aquí hay una clase que implementa la igualdad de valores:

 class Point : IEquatable { public int X { get; } public int Y { get; } public Point(int x = 0, int y = 0) { X = x; Y = y; } public bool Equals(Point other) { if (other is null) return false; return X.Equals(other.X) && Y.Equals(other.Y); } public override bool Equals(object obj) => Equals(obj as Point); public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs); public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs); public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); } 

Las únicas partes móviles en el código anterior son las partes en negrita: la segunda línea en Equals(Point other) y el método GetHashCode() . El otro código debe permanecer sin cambios.

Para las clases de referencia que no representan valores inmutables, no implemente los operadores == y != . En cambio, use su significado predeterminado, que es comparar la identidad del objeto.

El código equivale intencionalmente incluso objetos de un tipo de clase derivado. A menudo, esto puede no ser deseable porque la igualdad entre la clase base y las clases derivadas no está bien definida. Desafortunadamente, .NET y las pautas de encoding no son muy claras aquí. El código que Resharper crea, publicado en otra respuesta , es susceptible de comportamiento no deseado en tales casos porque Equals(object x) e Equals(SecurableResourcePermission x) tratarán este caso de manera diferente.

Para cambiar este comportamiento, se debe insertar una verificación de tipo adicional en el método Equals tipo fuerte anterior:

 public bool Equals(Point other) { if (other is null) return false; if (other.GetType() != GetType()) return false; return X.Equals(other.X) && Y.Equals(other.Y); } 

A continuación he resumido lo que debe hacer al implementar IEquatable y proporcionó la justificación de las diversas páginas de documentación de MSDN.


Resumen

  • Cuando se desea probar la igualdad de valores (como al usar objetos en colecciones), debe implementar la interfaz IEquatable, anular Object.Equals y GetHashCode para su clase.
  • Cuando desee realizar pruebas de igualdad de referencia, debe usar operator ==, operator! = Y Object.ReferenceEquals .
  • Solo debe anular operador == y operador! = Para ValueTypes y tipos de referencia inmutables.

Justificación

IEquatable

La interfaz System.IEquatable se usa para comparar dos instancias de un objeto para la igualdad. Los objetos se comparan en función de la lógica implementada en la clase. La comparación da como resultado un valor booleano que indica si los objetos son diferentes. Esto está en contraste con la interfaz System.IComparable, que devuelve un número entero que indica cómo los valores del objeto son diferentes.

La interfaz IEquatable declara dos métodos que deben ser anulados. El método Equals contiene la implementación para realizar la comparación real y devolver verdadero si los valores del objeto son iguales, o falso si no lo son. El método GetHashCode debe devolver un valor hash exclusivo que se puede usar para identificar de manera única objetos idénticos que contienen valores diferentes. El tipo de algoritmo de hash utilizado es específico de la implementación.

Método IEquatable.Equals

  • Debe implementar IEtabletable para que sus objetos controlen la posibilidad de que se almacenen en una matriz o colección genérica.
  • Si implementa IEquatable, también debe anular las implementaciones de clase base de Object.Equals (Object) y GetHashCode para que su comportamiento sea coherente con el del método IEquatable.Equals

Directrices para anular Iguales () y Operador == (Guía de progtwigción C #)

  • x.Equals (x) devuelve verdadero.
  • x.Equals (y) devuelve el mismo valor que y.Equals (x)
  • if (x.Equals (y) && y.Equals (z)) devuelve verdadero, entonces x.Equals (z) devuelve verdadero.
  • Sucesivas invocaciones de x. Igual (y) devuelve el mismo valor siempre que los objetos referenciados por x e y no sean modificados.
  • X. Igual (nulo) devuelve falso (solo para tipos de valores que no admiten nulos . Para obtener más información, vea Tipos anulables (Guía de progtwigción C #) .)
  • La nueva implementación de Equals no debería arrojar excepciones.
  • Se recomienda que cualquier clase que anule Igual también anule Object.GetHashCode.
  • Se recomienda que, además de implementar Equals (objeto), cualquier clase también implemente Equals (type) para su propio tipo, para mejorar el rendimiento.

Por defecto, el operador == prueba la igualdad de referencia al determinar si dos referencias indican el mismo objeto. Por lo tanto, los tipos de referencia no tienen que implementar operator == para obtener esta funcionalidad. Cuando un tipo es inmutable, es decir, los datos contenidos en la instancia no pueden modificarse, la sobrecarga del operador == para comparar la igualdad de valores en lugar de la igualdad de referencia puede ser útil porque, como objetos inmutables, pueden considerarse iguales que largos ya que tienen el mismo valor. No es una buena idea anular operador == en tipos no inmutables.

  • Las implementaciones de operador == sobrecargado no deberían arrojar excepciones.
  • ¡Cualquier tipo que sobrecarga al operador == también debería sobrecargar al operador! =.

== Operador (Referencia de C #)

  • Para tipos de valores predefinidos, el operador de igualdad (==) devuelve verdadero si los valores de sus operandos son iguales, de lo contrario es falso.
  • Para 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.
  • Cuando se prueba nulo usando comparaciones == dentro de su operador == anula, asegúrese de usar el operador de clase de objeto base. Si no lo hace, ocurrirá una recursión infinita que dará como resultado un stackoverflow.

Método Object.Equals (Object)

Si su lenguaje de progtwigción admite la sobrecarga del operador y si elige sobrecargar el operador de igualdad para un tipo determinado, ese tipo debe anular el método Equals. Tales implementaciones del método Equals deben devolver los mismos resultados que el operador de igualdad

Las siguientes pautas son para implementar un tipo de valor :

  • Considere anular Igual para obtener un mayor rendimiento que el proporcionado por la implementación predeterminada de Igual en ValueType.
  • Si anula Equals y el idioma admite la sobrecarga del operador, debe sobrecargar el operador de igualdad para su tipo de valor.

Las siguientes pautas son para implementar un tipo de referencia :

  • Considere anular Iguales en un tipo de referencia si la semántica del tipo se basa en el hecho de que el tipo representa algún valor (es).
  • La mayoría de los tipos de referencia no deben sobrecargar el operador de igualdad, incluso si anula Igual. Sin embargo, si está implementando un tipo de referencia que pretende tener semántica de valores, como un tipo de número complejo, debe anular el operador de igualdad.

Gotchas adicionales

  • Al anular GetHashCode () asegúrese de probar los tipos de referencia para NULL antes de usarlos en el código hash.
  • Me encontré con un problema con la progtwigción basada en la interfaz y la sobrecarga del operador que se describe aquí: Sobrecarga del operador con la progtwigción basada en la interfaz en C #

Ese artículo simplemente recomienda no anular el operador de igualdad (para tipos de referencia), no en contra de anular Iguales. Debe anular Igual dentro de su objeto (referencia o valor) si las verificaciones de igualdad significarán algo más que verificaciones de referencia. Si desea una interfaz, también puede implementar IEquatable (utilizado por colecciones genéricas). Sin embargo, si implementa IEquatable, también debe anular equals, como lo indica la sección de comentarios de IEquatable:

Si implementa IEquatable , también debe anular las implementaciones de clase base de Object.Equals (Object) y GetHashCode para que su comportamiento sea coherente con el del método IEquatable .Equals. Si anula Object.Equals (Object), su implementación anulada también se llama en llamadas al método estático Equals (System.Object, System.Object) en su clase. Esto garantiza que todas las invocaciones del método Equals devuelvan resultados consistentes.

En cuanto a si debe implementar Equals y / o el operador de igualdad:

De Implementar el Método de Igualdad

La mayoría de los tipos de referencia no deben sobrecargar al operador de igualdad, incluso si anula Igual.

De las Pautas para la implementación de Equals y el operador de igualdad (==)

Reemplace el método Equals siempre que implemente el operador de igualdad (==) y haga que hagan lo mismo.

Esto solo indica que debe anular Equals siempre que implemente el operador de igualdad. No dice que deba anular el operador de igualdad cuando anula Equals.

Para objetos complejos que producirán comparaciones específicas, la implementación de IComparable y la definición de la comparación en los métodos de comparación es una buena implementación.

Por ejemplo, tenemos objetos “Vehículo” donde la única diferencia puede ser el número de registro y lo usamos para comparar para garantizar que el valor esperado devuelto en la prueba sea el que deseamos.

Tiendo a usar lo que Resharper hace automáticamente. por ejemplo, se creó automáticamente para uno de mis tipos de referencia:

 public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj); } public bool Equals(SecurableResourcePermission obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny); } public override int GetHashCode() { unchecked { int result = (int)ResourceUid; result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0); result = (result * 397) ^ AllowDeny.GetHashCode(); return result; } } 

Si desea anular == y seguir haciendo comprobaciones de ref, aún puede usar Object.ReferenceEquals .

Microsoft parece haber cambiado su tono, o al menos hay información contradictoria sobre no sobrecargar el operador de igualdad. De acuerdo con este artículo de Microsoft titulado How to: Define Value Equality para un tipo:

“Los operadores == y! = Pueden usarse con clases incluso si la clase no los sobrecarga. Sin embargo, el comportamiento predeterminado es realizar una verificación de igualdad de referencia. En una clase, si sobrecarga el método Equals, debe sobrecargar el == y! = operadores, pero no es obligatorio “.

Según Eric Lippert en su respuesta a una pregunta, le pregunté sobre el código Minimal para la igualdad en C # – él dice:

“El peligro que se encuentra aquí es que obtiene un operador == definido para usted que hace referencia a la igualdad por defecto. Podría terminar fácilmente en una situación en la que un método Equals sobrecargado valora la igualdad y == hace referencia a la igualdad, y luego accidentalmente usa la igualdad de referencia en elementos que no son de igual relación y que son iguales a los valores. Esta es una práctica propensa a errores que es difícil de detectar por la revisión de código humano.

Hace un par de años trabajé en un algoritmo de análisis estático para detectar estadísticamente esta situación, y encontramos una tasa de defectos de aproximadamente dos instancias por millón de líneas de código en todas las bases de código que estudiamos. Al considerar solo las bases de código que tenían un lugar reemplazado por Iguales, ¡la tasa de defectos obviamente era considerablemente más alta!

Además, considere los costos frente a los riesgos. Si ya tiene implementaciones de IComparable, escribir todos los operadores es trivial y no tendrá errores y nunca se modificará. Es el código más barato que vas a escribir. Si se le da la opción entre el costo fijo de escribir y probar una docena de métodos pequeños versus el costo ilimitado de encontrar y corregir un error difícil de ver cuando se usa la igualdad de referencia en lugar de la igualdad de valores, sé cuál elegiría “.

.NET Framework nunca usará == o! = Con ningún tipo que escriba. Pero, el peligro es qué pasaría si alguien más lo hace. Entonces, si la clase es para un tercero, siempre proporcionaría los operadores == y! =. Si la clase solo está destinada a ser utilizada internamente por el grupo, aún así implementaría los operadores == y! =.

Solo implementaría los operadores <, <=,> y> = si se implementó IComparable. IComparable solo debe implementarse si el tipo necesita admitir pedidos, como al ordenar o usar en un contenedor genérico ordenado como SortedSet.

Si el grupo o la compañía tuvieran una política para no implementar nunca los operadores == y! =, Entonces, por supuesto, seguiría esa política. Si dicha política estuviera en su lugar, sería aconsejable aplicarla con una herramienta de análisis de código Q / A que señale cualquier ocurrencia de los operadores == y! = Cuando se utiliza con un tipo de referencia.

Creo que obtener algo tan simple como verificar los objetos para la igualdad correcta es un poco complicado con el diseño de .NET.

Para Struct

1) Implementar IEquatable . Mejora notablemente el rendimiento.

2) Como ahora tiene sus propios Equals , anule GetHashCode y sea coherente con varios object.Equals anulación de control de igualdad. object.Equals también.

3) Sobrecarga == y != operadores no necesitan ser hechos religiosamente ya que el comstackdor advertirá si usted involuntariamente equipara una estructura con otra con a == o != , Pero es bueno hacerlo para ser consistente con los métodos de Equals .

 public struct Entity : IEquatable { public bool Equals(Entity other) { throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { if (obj == null || !(obj is Entity)) return false; return Equals((Entity)obj); } public static bool operator ==(Entity e1, Entity e2) { return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Para clase

De MS:

La mayoría de los tipos de referencia no deben sobrecargar al operador de igualdad, incluso si anula Igual.

Para mí == siente como igualdad de valor, más como un azúcar sintáctico para el método Equals . Escribir a == b es mucho más intuitivo que escribir a.Equals(b) . En raras ocasiones necesitaremos verificar la igualdad de referencia. En niveles abstractos que tratan con representaciones lógicas de objetos físicos, esto no es algo que necesitemos verificar. Creo que tener semántica diferente para == e Equals puede ser realmente confusa. Creo que debería haber sido == para igualdad de valor e Equals para referencia (o un nombre mejor como IsSameAs ) igualdad en primer lugar. Me encantaría no tomar en serio la guía de MS aquí, no solo porque no es natural para mí, sino también porque la sobrecarga == no causa ningún daño importante. Eso es diferente de no anular Equals no genérico o GetHashCode que puede afectar, porque framework no usa == ninguna parte, pero solo si nosotros mismos lo usamos. El único beneficio real que obtengo al no sobrecargar == y != Será la coherencia con el diseño de todo el marco sobre el que no tengo control. Y eso es realmente algo grande, así que tristemente me apegaré a eso .

Con semántica de referencia (objetos mutables)

1) Anular Equals y GetHashCode .

2) Implementar IEquatable no es obligatorio, pero será bueno si tienes uno.

 public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classes, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Con semántica de valores (objetos inmutables)

Esta es la parte difícil. Se puede confundir fácilmente si no se tiene cuidado ..

1) Anular Equals y GetHashCode .

2) Sobrecarga == y != Para coincidir con Equals . Asegúrate de que funcione para los nulos .

2) Implementar IEquatable no es obligatorio, pero será bueno si tienes uno.

 public class Entity : IEquatable { public bool Equals(Entity other) { if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(null, other)) return false; //if your below implementation will involve objects of derived classes, then do a //GetType == other.GetType comparison throw new NotImplementedException("Your equality check here..."); } public override bool Equals(object obj) { return Equals(obj as Entity); } public static bool operator ==(Entity e1, Entity e2) { if (ReferenceEquals(e1, null)) return ReferenceEquals(e2, null); return e1.Equals(e2); } public static bool operator !=(Entity e1, Entity e2) { return !(e1 == e2); } public override int GetHashCode() { throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here..."); } } 

Tenga especial cuidado para ver cómo le iría si su clase se puede heredar, en tales casos tendrá que determinar si un objeto de clase base puede ser igual a un objeto de clase derivado. Idealmente, si no se usan objetos de clase derivada para la comprobación de igualdad, una instancia de clase base puede ser igual a una instancia de clase derivada y, en tales casos, no es necesario verificar la igualdad de Type en igualdad genérica de clase base.

En general, tenga cuidado de no duplicar el código. Podría haber hecho una clase base abstracta genérica ( IEqualizable o menos) como plantilla para permitir la reutilización más fácil, pero tristemente en C # que me impide derivar de clases adicionales.

Es notable lo difícil que es hacer las cosas bien …

La recomendación de Microsoft de que Equals y == hagan cosas diferentes en este caso no tiene sentido para mí. En algún momento, alguien esperará (con razón) Igual y == para producir el mismo resultado y el código se bombardeará.

Estaba buscando una solución que:

  • producir el mismo resultado si se usa igual o == en todos los casos
  • ser completamente polimórfico (llamando igualdad derivada a través de referencias base) en todos los casos

Se me ocurrió esto:

 class MyClass : IEquatable { public int X { get; } public int Y { get; } public MyClass(int x = 0, int y = 0) { X = x; Y = y; } public override bool Equals(object obj) { var o = obj as MyClass; return o is null ? false : X.Equals(oX) && Y.Equals(oY); } public bool Equals(MyClass o) => object.Equals(this, o); public static bool operator ==(MyClass o1, MyClass o2) => object.Equals(o1, o2); public static bool operator !=(MyClass o1, MyClass o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(X, Y); } 

Aquí todo termina en Equals(object) que es siempre polimórfico, por lo que se logran ambos objectives.

Derive así:

 class MyDerived : MyClass, IEquatable { public int Z { get; } public int K { get; } public MyDerived(int x = 0, int y = 0, int z=0, int k=0) : base(x, y) { Z = z; K = k; } public override bool Equals(object obj) { var o = obj as MyDerived; return o is null ? false : base.Equals(obj) && Z.Equals(oZ) && K.Equals(oK); } public bool Equals(MyDerived other) => object.Equals(this, o); public static bool operator ==(MyDerived o1, MyDerived o2) => object.Equals(o1, o2); public static bool operator !=(MyDerived o1, MyDerived o2) => !object.Equals(o1, o2); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K); } 

Que es básicamente el mismo excepto por un gotcha – cuando Equals(object) quiere llamar a base. base.Equals deben tener cuidado de llamar a base.Equals(object) y no base.Equals(MyClass) (lo que provocará una recursividad sin fin).

Una advertencia aquí es que Equals(MyClass) en esta implementación hará algo de boxeo; sin embargo, el boxeo / unboxing está muy optimizado y para mí vale la pena lograr los objectives anteriores.

demo: https://dotnetfiddle.net/cCx8WZ

(Tenga en cuenta que es para C #> 7.0)
(basado en la respuesta de Konard)