El uso de Dictionary<TKey, TValue> en .NET es fundamental cuando se requiere una estructura de datos que permita búsquedas rápidas mediante pares clave-valor. Aunque los tipos primitivos como string o int suelen ser suficientes, existen escenarios complejos donde necesitamos utilizar objetos de nuestras propias clases como identificadores únicos dentro de un diccionario.
Mecánica interna del Dictionary
Para comprender por qué una clase personalizada no funciona como clave de forma automática, es necesario entender cómo opera el diccionario internamente. Esta estructura se basa en una Tabla Hash.
Cuando insertamos o buscamos un elemento, el diccionario realiza dos pasos críticos:
- Obtención del Hash: Llama al método
GetHashCode()del objeto para determinar en qué "cubeta" (bucket) o dirección de memoria debería estar almacenado el valor. - Verificación de Igualdad: Si varios objetos terminan en la misma cubeta (lo que se conoce como colisión), el diccionario utiliza el método
Equals()para iterar y encontrar la clave exacta.
Por defecto, en .NET, las clases heredan estos métodos de System.Object. La implementación base de Object compara la referencia de memoria. Esto significa que dos objetos distintos con los mismos valores internos serán tratados como claves diferentes.
El problema de la igualdad por referencia
Consideremos el siguiente ejemplo donde intentamos usar una clase sin sobrescribir sus métodos de comparación:
using System;
using System.Collections.Generic;
public class Producto
{
public int Codigo { get; set; }
public string Nombre { get; set; }
public Producto(int codigo, string nombre)
{
Codigo = codigo;
Nombre = nombre;
}
}
public class Ejemplo
{
public static void Main()
{
var inventario = new Dictionary<Producto, int>();
var p1 = new Producto(500, "Monitor");
inventario.Add(p1, 10);
// Intentamos buscar con un objeto que tiene los mismos datos
var p2 = new Producto(500, "Monitor");
Console.WriteLine(inventario.ContainsKey(p2)); // Resultado: False
}
}
A pesar de que p1 y p2 representan el mismo producto lógicamente, el diccionario devuelve False porque sus referencais en memoria son distintas.
Sobrescritura de Equals y GetHashCode
Para que el diccionario reconozca dos instancias distintas como la misma clave, debemos definir qué constiutye la "igualdad" para nuestra clase. Esto se logra sobrescribiendo Equals(object obj) y GetHashCode().
public class Producto
{
public int Codigo { get; set; }
public string Nombre { get; set; }
public Producto(int codigo, string nombre)
{
Codigo = codigo;
Nombre = nombre;
}
// Definimos la lógica de comparación
public override bool Equals(object obj)
{
if (obj is Producto otroProducto)
{
return this.Codigo == otroProducto.Codigo &&
this.Nombre == otroProducto.Nombre;
}
return false;
}
// Generamos un código hash basado en las propiedades
public override int GetHashCode()
{
unchecked // Evita desbordamientos
{
int hash = 17;
hash = hash * 23 + (Nombre != null ? Nombre.GetHashCode() : 0);
hash = hash * 23 + Codigo.GetHashCode();
return hash;
}
}
}
Puntos clavee para la implementación
- Consistencia: Si dos objetos son iguales según
Equals(), deben devolver obligatoriamente el mismo valor enGetHashCode(). - Inmutabilidad: Es una buena práctica que las propiedades utilizadas para calcular el Hash Code sean inmutables (readonly). Si los valores de la clave cambian mientras el objeto está dentro del diccionario, el Hash Code cambiará y el objeto se volverá "inlocalizable".
- Rendimiento: El método
GetHashCodedebe ser rápido, ya que se invoca frecuentemente durante las operaciones de lectura y escritura en el diccionario.
Al implementar estos cambios, el código anterior devolverá True, permitiendo que nuestras clases de negocio actúen como identificadores eficientes y lógicos en cualquier colección basada en hash en .NET.