C# ¿por qué es necesario crear Equals() y GetHashCode()?
Tags: spanish, csharp, tutorial, programming
Published: 2023-11-17 15:37:59

---
title: C# ¿por qué es necesario crear Equals() y GetHashCode()?
published: true
description: Creando Equals() y GetHashCode() en C#
tags: #spanish #csharp #tutorial #programming
cover_image: https://github.com/dotnet/vscode-csharp/raw/main/images/csharpIcon.png
---

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()`](https://learn.microsoft.com/es-es/dotnet/api/system.object.equals) y otro [`GetHashCode()`](https://learn.microsoft.com/es-es/dotnet/api/system.object.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.

```c#
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:

```c#
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:

```c#
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*.

```c#
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?

```c#
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**.

```c#
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.

```c#
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.