Tracks
/
C#
C#
/
Temario
/
Igualdad
Ig

Igualdad en C#

5 ejercicios

Acerca de Igualdad

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

Object.Equals()

Temas que cubre el ejercicio

  • Los tipos simples (strings y primitivos) normalmente se comparan 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. Quienes programan en Java deben tener en cuenta, cuando trabajan con strings, que == compara por valor en C# pero por referencia en Java al volver a su lenguaje anterior.
  • Los tipos de referencia (instancias de clases) se comparan usando 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 es así, necesitas sobrescribir object.Equals().
  • Si sabes que todas las instancias de tu clase se crean en un solo lugar, por ejemplo personajes en 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, desde una base de datos, a partir de la entrada del usuario o mediante una solicitud web). En este caso, los valores que identifican de forma única a la entidad deben comprobarse para ver si son iguales. Por lo tanto, se debe sobrescribir Equals().
  • Un Equals() sobrescrito contendrá pruebas de igualdad sobre miembros de tipos simples usando ==, y sobre tipos de referencia con 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 misma instancia. Esto aporta claridad y es una necesidad cuando se han sobrescrito o sobrecargado Equals() y ==.
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 complementarios

  • Además de public override bool Equals(object obj), los IDE normalmente generan 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 agregar su propia prueba.
  • No uses == a menos que hayas sobrecargado el operador ==, además del método Equals() en tu clase (consulta el ejercicio operator-overloading), o a menos que solo te importe que las referencias sean iguales.
  • Las pruebas de igualdad en structs se tratan en el ejercicio structs.
  • La igualdad de tuples se trata en el ejercicio tuples.
  • Muchos desarrolladores confían en que su IDE proporcione la implementación de los métodos de igualdad, ya que estos se encargan de todos los detalles 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. Ese código suele ser a prueba de valores nulos y de objetos mal tipados, y está bastante bien optimizado.
  • Las pruebas de igualdad de delegados no se tratan específicamente en el ejercicio.
  • No hay pruebas de igualdad integradas para arrays ni para la mayoría de las colecciones. LINQ (que se trata en ejercicios posteriores) proporciona SequenceEquals(), pero en ausencia de LINQ se trata de iterar por ambas colecciones y comparar los elementos uno por uno.
  • Para ver 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 se relaciona con las claves.
  • Entre quienes programan en C# se espera que, si sobrescribes Equals(), también sobrescribas GetHashCode(). Existe una relación entre Equals() y GetHashCode() que debe cumplirse para el comportamiento correcto de las clases de diccionario y de conjunto de hash, y de cualquier otra que use un código hash. Se espera que implementes el método de modo que no dejes trampas para quienes mantienen el código y que podrían agregar 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), entonces GetHashCode() debe devolver el mismo valor para ambos objetos. Esto no se aplica en la dirección inversa. No es simétrico. Imagina una función de búsqueda que primero va a un «bucket» según el código hash y luego elige el elemento concreto usando la prueba de igualdad.
  • La forma más fácil de crear un código hash es llamar a HashCode.Combine() pasando los valores usados en la prueba de igualdad (o un subconjunto). Ten en cuenta que, cuanta más información 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 usa la colección con hash. Si agregas 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. Otros enfoques corren el riesgo de crear trampas para quienes mantienen el código. 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 eso será porque tienes un conocimiento detallado de las características de los datos o porque es una colección muy simple en la que los valores se pueden usar directamente sin aplicar hash. Puede que no valga la pena el esfuerzo adicional.

Mejoras de rendimiento

Para mejorar un poco el rendimiento, sobre todo cuando los objetos pertenecen a colecciones, puedes agregar 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 agregas la interfaz IEquatable<T> a tu clase, esto requerirá que se implemente la sobrecarga. A menos que tu código contenga rutinas que acepten objetos de tipo IEquatable<T> (y que, supuestamente, hagan algo interesante relacionado con la igualdad, sea cual sea 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 se puede identificar 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 diferentes usen pruebas de igualdad y códigos hash distintos. 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 cuya clave sea el SSID y otro cuya clave sea la dirección de correo electrónico.

Cuando entra en juego IEqualityComparer<T>, normalmente seguirás sobrescribiendo Equals() y GetHashCode() en tu clase de 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 lugar, sería aún más fácil que quien mantiene el código haga cambios en los miembros de una clase sin tener en cuenta las consecuencias para la colección.

Nota sobre la igualdad de números de punto flotante

Un primitivo que puede poner a prueba a quien programa sin estar atento es comprobar la igualdad de valores de punto 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 con respecto a 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 ventana o pestaña nueva

Aprende Igualdad

La práctica está bloqueada

Desbloquea 4 ejercicios más para practicar Igualdad