Tracks
/
C#
C#
/
Temario
/
Igualdad
Ig

Igualdad en C#

5 ejercicios

Acerca de Igualdad

El ejercicio ilustra varias propiedades de la igualdad en C#:

Object.Equals()

Temas que cubre el ejercicio

  • Los tipos simples (los strings y los primitivos) suelen compararse para ver si son iguales con == y !=. Esto se considera más idiomático que usar el método Equals(), que también está disponible con estos tipos. Los desarrolladores de Java deberían estar atentos, cuando trabajan con strings, al hecho de que == compara por valor en C#, pero por referencia en Java, cuando vuelven a su antiguo lenguaje.
  • Los tipos de referencia (las instancias de clases) se comparan con el método Equals() heredado de object. Si tu objetivo con la prueba de igualdad es asegurarte de que dos objetos son exactamente la misma instancia, basta con confiar en la implementación de object. Si no, tendrás que sobrescribir object.Equals().
  • Si sabes que todas las instancias de tu clase se crean en un solo sitio, por ejemplo los personajes de algún juego o simulación, la igualdad por referencia es suficiente. Sin embargo, es probable que se creen varias instancias de la misma entidad del mundo real (por ejemplo, a partir de una base de datos, de la entrada del usuario o de una petición web). En ese caso hay que comparar los valores que identifican de forma única a la entidad. Por lo tanto, hay que sobrescribir Equals().
  • Un Equals() sobrescrito contendrá comparaciones de igualdad de miembros de tipos simples mediante == y de tipos de referencia mediante llamadas recursivas a Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • El método estático object.ReferenceEquals() se usa para comparar dos objetos y detectar si son una y la misma instancia. Esto aporta claridad y es necesario cuando Equals() y == se han sobrescrito o sobrecargado.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

Temas auxiliares

  • Además de public override bool Equals(object obj), los IDE suelen generar la sobrecarga protected bool Equals(FacialFeatures other) para usarla cuando hay herencia. Una clase derivada puede llamar al Equals() de la clase base y luego añadir su propia comprobación.
  • No uses == a menos que hayas sobrecargado el operador ==, además del método Equals() de tu clase (consulta el ejercicio operator-overloading), o a menos que solo te importe que las referencias sean iguales.
  • Las comparaciones de igualdad en los structs se tratan en el ejercicio structs.
  • La igualdad de las tuples se trata en el ejercicio tuples.
  • Muchos desarrolladores confían en que sus IDE implementen los métodos de igualdad, ya que estos se encargan de todas las minucias de la igualdad. Por ejemplo, RIDER de JetBrains (v 2020.1) genera los siguientes métodos de igualdad para una clase:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • Ten cuidado si decides mejorar el código que genera tu IDE. Por lo general, ese código es a prueba de nulos y de objetos mal tipados, y está bastante bien optimizado.
  • Las comprobaciones de igualdad de los delegados no se tratan específicamente en el ejercicio.
  • No hay comparaciones de igualdad integradas para los arrays ni para la mayoría de las colecciones. LINQ (que se trata en ejercicios posteriores) proporciona SequenceEquals(), pero si no hay LINQ, habrá que recorrer ambas colecciones y comparar los elementos uno a uno.
  • Para saber cómo usar == y != con tus propias clases, consulta el ejercicio operator-overloading.

Object.GetHashCode()

  • object.GetHashCode() devuelve un código hash en forma de entero de 32 bits. Las clases de diccionario y de conjunto, como Dictionary<T> y HashSet<T>, usan el código hash para almacenar y recuperar objetos de forma eficiente. En el caso de los diccionarios, el hashing está relacionado con las claves.
  • Existe la expectativa entre los desarrolladores de C# de que, si sobrescribes Equals(), también sobrescribas GetHashCode(). Hay una relación entre Equals() y GetHashCode() que debe cumplirse para el correcto funcionamiento de las clases de diccionario y de conjunto hash, y de cualquier otra que use un código hash. Se espera que implementes el método de forma que no se tiendan trampas a los responsables del mantenimiento que puedan añadir más adelante una colección basada en códigos hash.
  • La relación entre el código hash y la igualdad es que, si dos objetos son iguales (Equal() devuelve true), GetHashCode() debe devolver el mismo valor para ambos objetos. Esto no se aplica en la dirección contraria. No es simétrico. Imagina una función de búsqueda que primero va a un «bucket» basándose en el código hash y luego selecciona el elemento concreto mediante la prueba de igualdad.
  • La forma más sencilla de crear un código hash es llamar a HashCode.Combine() y pasarle los valores usados en la prueba de igualdad (o un subconjunto). Ten en cuenta que, cuanta más información le proporciones a Combine(), más eficiente será probablemente la implementación del hash.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • Los valores usados en la prueba de igualdad deben ser estables mientras se esté usando la colección con hash. Si añades un objeto a la colección con un conjunto de valores y luego cambias esos valores, el código hash ya no apuntará al «bucket» correcto. En la práctica, esto significa que el objeto debería ser inmutable. Otras estrategias corren el riesgo de crear sorpresas desagradables a los responsables del mantenimiento. La inmutabilidad se trata en otros ejercicios.
  • Es posible que puedas diseñar un código hash mejor que el que producen las rutinas de la biblioteca, pero o bien es porque conoces en detalle las características de los datos, o bien porque se trata de una colección muy simple en la que los valores pueden usarse directamente, sin aplicarles un hash. Puede que no merezca la pena el esfuerzo adicional.

Mejoras de rendimiento

Para mejorar ligeramente el rendimiento, sobre todo cuando los objetos pertenecen a colecciones, puedes añadir un miembro sobrecargado public bool Equals(T other).

Esto ahorrará cierta cantidad de comprobaciones de tipo para los tipos de referencia y ahorrará un paso de boxing para los tipos de valor, ya que no será necesario convertirlos a un objeto (boxing) como argumento de public override bool Equals(object other).

Si añades la interfaz IEquatable<T> a tu clase, tendrás que implementar la sobrecarga. A menos que tu código contenga rutinas que reciban objetos de tipo IEquatable<T> (y que presuntamente hagan algo interesante relacionado con la igualdad, independientemente de la clase que lo implemente), en realidad no hay ninguna otra razón de peso para incluir la interfaz.

IEqualityComparer<T>

Si tienes una clase que puede identificarse de forma única de dos maneras distintas, por ejemplo una clase Person que tiene un SSID y una dirección de correo electrónico única, .NET ofrece un mecanismo para que dos colecciones distintas usen pruebas de igualdad y códigos hash diferentes. Cada una puede tener una implementación diferente de IEqualityComparer<T> con su propio método Equals() y su propio GetHashCode(). Puedes tener un diccionario con clave de SSID y otro con clave de dirección de correo electrónico.

Cuando IEqualityComparer<T> está en juego, lo habitual sigue siendo sobrescribir Equals() y GetHashCode() en la clase de tu elemento para evitar problemas fuera de las clases de colección.

Una consideración al usar IEqualityComparer<T> es que los métodos privados, etc., no estarán disponibles para la prueba de igualdad.

Si solo hay una colección con hash en juego, puede ser mejor evitar IEqualityComparer<T> y encapsular la igualdad y el código hash en el propio objeto. No es lo ideal, para empezar, que el compilador no pueda hacer cumplir una dependencia clave como la que existe entre el objeto y la colección con hash, pero si la lógica de hash y de comparación está en otro sitio, aún sería más fácil que un responsable del mantenimiento modificara los miembros de una clase sin tener en cuenta las consecuencias para la colección.

Nota sobre la igualdad de coma flotante

Un tipo primitivo que puede sorprender a quien no tenga cuidado es comprobar la igualdad de valores de coma flotante. Esto se trata en el documento about.md del concepto floating-point-numbers.

Igualdad y herencia

Este artículo muestra algunas de las decisiones que hay que tomar en relación con la igualdad y la herencia. Solo te concernirán si estás manipulando instancias de una clase y de una clase derivada y necesitas comprobar la igualdad entre ambas.

Editar en GitHub El enlace se abre en una nueva ventana o pestaña

Aprende Igualdad

La práctica está bloqueada

Desbloquea 4 ejercicios más para practicar Igualdad