Pregunta

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

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

Mientras observa algunos de los pautas de estándares de codificación en MSDN me encontré con un artículo que lo desaconseja.Ahora entiendo por qué el artículo dice esto (porque no son lo mismo instancia) pero no responde a la pregunta:

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

Muchas gracias ^_^

Actualizar

Parece que había leído mal parte de la documentación (ha sido un día largo) y anulé igual puede ser el camino a seguir..

Si está implementando tipos de referencia, debe considerar anular el método igual en un tipo de referencia si su tipo parece un tipo base como un punto, cadena, número de número, etc.La mayoría de los tipos de referencia no deben sobrecargar el igualdad operador, incluso si anulan iguales.Sin embargo, si está implementando un tipo de referencia que tiene la intención de tener una semántica de valor, como un tipo de número complejo, debe anular el operador de igualdad.

¿Fue útil?

Solución

Parece que estás codificando en C#, que tiene un método llamado Equals que tu clase debería implementar, si deseas comparar dos objetos usando alguna otra métrica que no sea "¿son estos dos punteros (porque los identificadores de objetos son solo eso, punteros) a ¿la misma dirección de memoria?".

Tomé un código de muestra de 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 == p.x) && (y == p.y);
    }

    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 == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

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

La razón por la que sobrecargar '==' puede ser una mala idea para los objetos es que, por lo general, aún desea poder hacer comparaciones de "¿son estos el mismo puntero?".Por lo general, se utilizan para, por ejemplo, insertar un elemento en una lista donde no se permiten duplicados, y es posible que algunas de las cosas de su marco no funcionen si este operador está sobrecargado de una manera no estándar.

Otros consejos

Implementar la igualdad en .NET de forma correcta, eficiente y sin duplicación de código es difícil.Específicamente, para tipos de referencia con semántica de valor (es decir, tipos inmutables que tratan la equivalencia como igualdad), deberías implementar el System.IEquatable<T> interfaz, y debes implementar todas las diferentes operaciones (Equals, GetHashCode y ==, !=).

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

class Point : IEquatable<Point> {
    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(otro.X) && Y.Equals(otro.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 GetHashCode() método.El otro código debería permanecer sin cambios.

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

El código intencionalmente equipara incluso objetos de un tipo de clase derivada.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 codificación no son muy claras aquí.El código que crea Resharper, publicado en otra respuesta, es susceptible a comportamientos no deseados en tales casos porque Equals(object x) y Equals(SecurableResourcePermission x) voluntad Trate este caso de manera diferente.

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

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

A continuación, resumí lo que debe hacer al implementar IEquatable y proporcioné la justificación de las distintas páginas de documentación de MSDN.


Resumen

  • Cuando se desea probar la igualdad de valores (como cuando se usan objetos en colecciones), debe implementar la interfaz IEquatable, anular Object.Equals y GetHashCode para su clase.
  • Cuando desee probar la igualdad de referencia, debe usar operator==,operator!= y Objeto.ReferenciaEquals.
  • Sólo debe anular operator== y operator!= para Tipos de valor y tipos de referencia inmutables.

Justificación

IEquatable

La interfaz System.IEquatable se utiliza para comparar la igualdad de dos instancias de un objeto.Los objetos se comparan según la lógica implementada en la clase.La comparación da como resultado un valor booleano que indica si los objetos son diferentes.Esto contrasta con la interfaz System.IComparable, que devuelve un número entero que indica en qué se diferencian los valores del objeto.

La interfaz IEquatable declara dos métodos que deben anularse.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 único que puede usarse para identificar de forma única objetos idénticos que contienen valores diferentes.El tipo de algoritmo hash utilizado es específico de la implementación.

Método IEquatable.Equals

  • Debe implementar IEquatable para que sus objetos manejen 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 consistente con el del método IEquatable.Equals.

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

  • x.Equals(x) devuelve verdadero.
  • x.Equals(y) devuelve el mismo valor que y.Equals(x)
  • si (x.Equals(y) && y.Equals(z)) devuelve verdadero, entonces x.Equals(z) devuelve verdadero.
  • Invocaciones sucesivas de x.Igual a (y) devuelve el mismo valor siempre que los objetos a los que hacen referencia xey no se modifiquen.
  • X.Igual a (nulo) devuelve falso (solo para tipos de valores que no aceptan valores NULL).Para más información, ver Tipos que aceptan valores NULL (Guía de programación de C#).)
  • La nueva implementación de Equals no debería generar excepciones.
  • Se recomienda que cualquier clase que anule Equals también anule Object.GetHashCode.
  • Se recomienda que, además de implementar Equals(objeto), cualquier clase también implemente Equals(tipo) para su propio tipo, para mejorar el rendimiento.

De forma predeterminada, el operador == prueba la igualdad de referencias determinando si dos referencias indican el mismo objeto. Por lo tanto, los tipos de referencia no tienen que implementar el operador == para obtener esta funcionalidad.Cuando un tipo es inmutable, es decir, los datos contenidos en la instancia no se pueden cambiar, sobrecargar el operador == para comparar la igualdad de valores en lugar de la igualdad de referencia puede ser útil porque, como objetos inmutables, pueden considerarse iguales siempre y cuando ya que tienen el mismo valor. No es una buena idea anular el operador == en tipos no inmutables.

  • Operador sobrecargado == las implementaciones no deberían generar excepciones.
  • Cualquier tipo que sobrecargue al operador == también debería sobrecargar al operador !=.

== Operador (Referencia C#)

  • Para tipos de valores predefinidos, el operador de igualdad (==) devuelve verdadero si los valores de sus operandos son iguales, falso en caso contrario.
  • Para tipos de referencia distintos de cadena, == devuelve verdadero si sus dos operandos se refieren al mismo objeto.
  • Para el tipo de cadena, == compara los valores de las cadenas.
  • Al probar nulo usando comparaciones == dentro de su operador == anulaciones, asegúrese de usar el operador de clase de objeto base.Si no lo hace, se producirá una recursividad infinita que provocará un desbordamiento de pila.

Método Object.Equals (Objeto)

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

Las siguientes pautas son para implementar una tipo de valor:

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

Las siguientes pautas son para implementar una tipo de referencia:

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

Problemas adicionales

Ese artículo simplemente recomienda no anular el operador de igualdad (para tipos de referencia), no anular Iguales.Debe anular Equals dentro de su objeto (referencia o valor) si las comprobaciones de igualdad significarán algo más que comprobaciones 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 los iguales, como lo indica la sección de comentarios de IEquatable:

Si implementa IEquatable<T>, 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<T>.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.

Con respecto a si debes implementar Equals y/o el operador de igualdad:

De Implementando el método de iguales

La mayoría de los tipos de referencia no deberían sobrecargar el operador de igualdad, incluso si anulan Iguales.

De Directrices para la implementación de Equals y el Operador de Igualdad (==)

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

Esto solo dice que debes anular Equals cada vez que implementes el operador de igualdad.Lo hace no digamos que necesita anular el operador de igualdad cuando anula Iguales.

Para objetos complejos que producirán comparaciones específicas, implementar IComparable y definir 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 y garantizar que el valor esperado devuelto en las pruebas sea el que queremos.

Tiendo a usar lo que Resharper hace automáticamente.por ejemplo, creó esto 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 quieres anular == y aún haces verificaciones de referencia, aún puedes usar Object.ReferenceEquals.

Microsoft parece haber cambiado de opinión, o al menos hay información contradictoria sobre no sobrecargar el operador de igualdad.De acuerdo a esto Artículo de Microsoft titulado Cómo:Definir igualdad de valores para un tipo:

"Los operadores == y != se pueden usar 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 los operadores == y !=, pero no es obligatorio".

Según Eric Lippert en su respuesta a una pregunta que hice sobre Código mínimo para la igualdad en C# - él dice:

"El peligro que corre aquí es que obtiene un operador == definido que hace referencia a la igualdad de forma predeterminada.Fácilmente podría terminar 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 cosas que no son iguales y que tienen valores iguales.Esta es una práctica propensa a errores que es difícil de detectar mediante una revisión de código humana.

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 en algún lugar habían anulado Equals, ¡la tasa de defectos fue obviamente considerablemente mayor!

Además, considere los costos frente a los riesgos.Si ya tiene implementaciones de IComparable, entonces escribir todos los operadores es una frase trivial que no tendrá errores y nunca se cambiará.Es el código más barato que jamás escribirás.Si tuviera que elegir 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 donde se usa igualdad de referencia en lugar de igualdad de valores, sé cuál elegiría".

.NET Framework nunca usará == o != con ningún tipo que escriba.Pero el peligro es lo que sucedería si alguien más lo hiciera.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, probablemente aún implementaría los operadores == y !=.

Solo implementaría los operadores <, <=, > y >= si se implementara IComparable.IComparable solo debe implementarse si el tipo necesita admitir pedidos, como cuando se ordena o se usa en un contenedor genérico ordenado como SortedSet.

Si el grupo o la empresa tuviera una política para no implementar nunca los operadores == y !=, entonces, por supuesto, seguiría esa política.Si dicha política estuviera vigente, entonces sería prudente aplicarla con una herramienta de análisis de código de preguntas y respuestas que señale cualquier aparición de los operadores == y != cuando se usan con un tipo de referencia.

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

Para estructura

1) implementar IEquatable<T>.Mejora notablemente el rendimiento.

2) Ya que estás teniendo el tuyo propio Equals ahora, anular GetHashCode, y para ser coherente con varias anulaciones de verificación de igualdad object.Equals también.

3) Sobrecarga == y != Los operadores no necesitan ser hechos religiosamente ya que el compilador le advertirá si sin querer equipara una estructura con otra con un == o !=, pero es bueno hacerlo para ser coherente con Equals métodos.

public struct Entity : IEquatable<Entity>
{
    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 EM:

La mayoría de los tipos de referencia no deberían sobrecargar el operador de igualdad, incluso si anulan Iguales.

A mi == se siente como igualdad de valores, más como un azúcar sintáctico para Equals método.Escribiendo a == b es mucho más intuitivo que escribir a.Equals(b).Rara vez necesitaremos verificar la igualdad de referencia.En niveles abstractos que tratan con representaciones lógicas de objetos físicos, esto no es algo que debamos comprobar.Creo que tener una semántica diferente para == y Equals De hecho, puede resultar confuso.Creo que debería haber sido == por la igualdad de valores y Equals como referencia (o un nombre mejor como IsSameAs) igualdad en primer lugar. Me encantaría no tomarme en serio las directrices sobre EM, no sólo porque no es natural para mí, sino también porque me sobrecarga. == no causa ningún daño importante. Eso es diferente a no anular los no genéricos. Equals o GetHashCode que puede contraatacar, porque el marco no usa == en cualquier lugar excepto si nosotros mismos lo utilizamos.El único beneficio real que obtengo no sobrecargar == y != será la coherencia con el diseño de todo el marco sobre el cual no tengo control.Y eso es realmente algo muy importante, así que lamentablemente me apegaré a ello.

Con semántica de referencia (objetos mutables)

1) Anular Equals y GetHashCode.

2) Implementación IEquatable<T> No es imprescindible, pero sería bueno si tuvieras uno.

public class Entity : IEquatable<Entity>
{
    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 valor (objetos inmutables)

Esta es la parte complicada.Puede estropearse fácilmente si no se cuida.

1) Anular Equals y GetHashCode.

2) Sobrecarga == y != para igualar Equals. Asegúrate de que funcione para nulos.

2) Implementación IEquatable<T> No es imprescindible, pero sería bueno si tuvieras uno.

public class Entity : IEquatable<Entity>
{
    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 en ver cómo debería funcionar 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 derivada.Idealmente, si no se utilizan objetos de clase derivada para verificar la igualdad, entonces una instancia de clase base puede ser igual a una instancia de clase derivada y, en tales casos, no hay necesidad de verificar. Type igualdad en generico Equals de clase base.

En general, tenga cuidado de no duplicar el código.Podría haber creado una clase base abstracta genérica (IEqualizable<T> más o menos) como plantilla para permitir una reutilización más sencilla, pero lamentablemente en C# eso me impide derivar clases adicionales.

Todas las respuestas anteriores no consideran el polimorfismo; a menudo desea que las referencias derivadas utilicen los iguales derivados incluso cuando se comparan a través de una referencia base.Consulte la pregunta/discusión/respuestas aquí: Igualdad y polimorfismo

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top