Implementación de clases personalizadas como claves en Dictionary de .NET

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:

  1. 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.
  2. 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 en GetHashCode().
  • 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 GetHashCode debe 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.

Etiquetas: C# .NET Dictionary GetHashCode equals

Publicado el 6-24 20:50