C# ¿por qué es necesario crear Equals() y GetHashCode()?
Baltasar García Perez-Schofield
Posted on November 17, 2023
Si deseamos crear una clase que, especialmente, podamos utilizar en las colecciones de la librería estándar, tendremos que suministrar un método Equals()
y otro GetHashCode()
. ¿Pero por qué esto es así?
Como ya conocemos, C# comenzó sobre todo como una nueva versión de Java desarrollada por Microsoft (léase versión, homenaje, reinterpretación...), puesto que la licencia de Java no permitía realizar modificaciones a la máquina virtual, algo en lo que Microsoft estaba muy interesada. Así, en Java, también existen estos dos métodos, y además por las mismas razones.
Como recurso para la explicación, utilizaremos la clase Polar, que representa coordenadas polares.
public class Polar {
public required int Angulo { get; init; }
public required int Distancia { get; init; }
public override string ToString()
{
return $"(@{this.Angulo}, {this.Distancia})";
}
}
Esta clase puede utilizarse de la siguiente forma:
var p1 = new Polar{ Angulo = 90, Distancia = 10 };
Console.WriteLine( p1 ); // (@90, 10)
En general, es posible que sintamos la necesidad de expresar si dos objetos Polar tienen los mismos valores de ángulo y distancia. Pero, si por algún motivo no preveemos la utilización de esta funcionalidad, no estaremos obligados a proporcionar una manera de comprobar si dos coordenadas son iguales. Sí que se convierte en obligatorio, sin embargo, si pensamos almacenar objetos de esta clase en las colecciones de la librería estándar en System.Collections
.
Cuando reescribimos alguno de estos dos métodos, se asume que se debe proveer del otro como una regla sencilla de recordar añadir esta funcionalidad sin que se presenten problemas. Aquí vamos a hacerlo por pasos para observar cómo afectan cada una de ellos.
Supongamos que creamos estos dos objetos:
var p1 = new Polar{ Angulo = 90, Distancia = 10 };
var p2 = new Polar{ Angulo = 90, Distancia = 10 };
...y además, creamos estas dos colecciones, en las que inmediatamente guardamos p1.
var s1 = new HashSet<Polar>();
var l1 = new List<Polar>();
l1.Add( p1 );
s1.Add( p1 );
Es interesante resaltar que estas dos colecciones son muy distintas entre sí. La primera se basa en un array que se va ampliando a medida que se introducen elementos. El segundo crea una serie de "carpetas", basadas en el valor de hashing del objeto, de forma que realizando un cálculo matemático, se obtiene la carpeta en la que guardarlo de manera directa, tanto para insertar el objeto como para recuperarlo.
Si ejecutamos el siguiente código, ¿qué sucedería?
if ( !( s1.Contains( p2 ) ) ) {
s1.Add( p2 );
Console.WriteLine( "p2 insertada en el conjunto." );
} else {
Console.WriteLine( "p2 no fue insertada en el conjunto" );
}
if ( !( l1.Contains( p2 ) ) ) {
l1.Add( p2 );
Console.WriteLine( "p2 insertada en la lista." );
} else {
Console.WriteLine( "p2 no fue insertada en la lista" );
}
Console.Write( "Elementos en el conjunto: " );
foreach (Polar polar in s1) {
Console.Write( polar );
Console.Write( " " );
}
Console.WriteLine();
Console.Write( "Elementos en la lista: " );
foreach (Polar polar in l1) {
Console.Write( polar );
Console.Write( " " );
}
Console.WriteLine();
La idea es insertar el objeto p2 (recordemos que tiene los mismo valores en Ángulo y Distancia que p1), en una lista l1 y en conjunto s1. La salida de este programa es la siguiente:
p2 insertada en el conjunto.
p2 insertada en la lista.
Elementos en el conjunto: (@90, 10) (@90, 10)
Elementos en la lista: (@90, 10) (@90, 10)
A pesar de que ambos objetos contienen los mismos valores, y a pesar de que antes de insertar se comprueba con el método Contains(x)
tanto en la lista como en el conjunto. Ahora sí vemos claramente la necesidad de ofrecer una manera de verificar la concordancia de dos objetos Polar.
Añadamos ahora la reescritura del método Equals(x)
a la clase Polar.
class Polar {
// más cosas...
public override bool Equals(object? obj)
{
return obj is Polar otro
&& this.Angulo == otro.Angulo
&& this.Distancia == otro.Distancia;
}
}
Este método reescribe el método Object.Equals(x)
, por lo que es necesario añadir el modificador override
. Es muy importante comprobar que el parámetro obj está marcado como object?
. La interrogación al final indica que obj* podría ser null. Se hace uso del operador is para comprobar si el objeto pertenece a la clase Polar. Esta comprobación falla si la referencia obj repressenta a un objeto que pertenece a otra clase o también si obj contiene null. En caso de que obj represente a un objeto de la clase Polar, se crea un objeto llamado otro que ya nos permite comparar las propiedades de ambos objetos.
Ahora la ejecución del código es la siguiente:
Test Polar
p2 insertada en el conjunto.
p2 no fue insertada en la lista
Elementos en el conjunto: (@90, 10) (@90, 10)
Elementos en la lista: (@90, 10)
Aunque p2 sigue insertándose en el el conjunto, ya no se inserta en la lista. Esto es debido a que, mientras Set.Contains(x)
se basa en el código hash para saber qué carpeta le corresponde a un objeto, y comprobar si se encuentra allí; List.Contains(x)
, por otra parte, lo que hace es una búsqueda secuencial llamando al método Equals(x)
para cada elemento.
La segunda parte sería reescribir el método Object.GetHashCode()
, de manera que se cree un código de hashing verdaderamente único para cada combinación de valores Ángulo y Distancia. Nótese que además debemos distinguir entre objetos Polar que tengan intercambiados sus valores Ángulo y Distancia. Así, un objeto Polar con 90 de ángulo y 10 de distancia debe ser distinto de un objeto Polar con 10 de ángulo y 90 de distancia.
En general, `GetHashCode()' debe devolver un valor de hashing igual en el caso de dos objetos Polar iguales, y dos valores distintos cuando ambos objetos son distintos.
Una forma efectiva de crear un método `GetHashCode()' es utilizar una lista de valores primos (por ejemplo, 3, 7, 11, 13...), y multiplicar el valor de hashing para cada atributo en la clase, sumando todos los productos para obtener el producto final.
class Polar {
// más cosas...
public override int GetHashCode()
{
return ( 7 * this.Angulo.GetHashCode() )
+ ( 11 * this.Distancia.GetHashCode() );
}
}
¿Cuál es el resultado ahora?
Test Polar
p2 no fue insertada en el conjunto
p2 no fue insertada en la lista
Elementos en el conjunto: (@90, 10)
Elementos en la lista: (@90, 10)
Ahora el comportamiento del programa es el correcto. El objeto p2 no es insertado en ninguna de las dos colecciones, como se esperaba que sucediera.
Posted on November 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.