Un HashMap es una colección de pares clave-valor diseñada para buscar datos por su clave.
Si en el artículo anterior sufrimos un poco con los Strings, hoy volvemos a terreno conocido. Casi todos los lenguajes tienen una estructura de datos para almacenar pares clave-valor.
- En Python:
dict - En JavaScript:
ObjectoMap - En C#:
Dictionary<K, V> - En C++:
std::unordered_map
En Rust, esta estructura se llama HashMap.Es la herramienta perfecta cuando quieres buscar datos no por un índice numérico (como en un Vector), sino por una etiqueta, un nombre o cualquier otra clave única.
Crear un HashMap
Lo primero que debes saber es que HashMap es un poco menos “famoso” que Vec o String. No está incluido en el preludio (las herramientas que Rust carga automáticamente).
Para usarlo, debemos importarlo explícitamente desde la librería estándar:
use std::collections::HashMap; // <--- Imprescindible
fn main() {
let mut puntuaciones = HashMap::new();
puntuaciones.insert(String::from("Azul"), 10);
puntuaciones.insert(String::from("Rojo"), 50);
}Al igual que los vectores, el HashMap reserva dinámicamente el almacenamiento de sus entradas y es homogéneo: todas las claves deben ser del mismo tipo, y todos los valores del mismo tipo.
Acceder a los valores
Para recuperar un dato usamos el método .get(), pasando una referencia a la clave.
let equipo = String::from("Azul");
let puntos = puntuaciones.get(&equipo); // Devuelve Option<&i32>Aquí Rust nos devuelve un Option<&i32>. ¿Por qué?
- Option: Porque la clave podría no existir (devuelve
None). - &i32 (Referencia): Porque el HashMap sigue siendo el dueño de los datos. Solo nos los está prestando para leerlos.
Podemos usar if let o match para manejar el resultado:
if let Some(puntos) = puntuaciones.get(&equipo) {
println!("El equipo tiene {} puntos", puntos);
} else {
println!("El equipo no existe");
}HashMap y ownership
En este punto Rust se comporta diferente a C# o Java. En esos lenguajes, si usas un objeto como clave, simplemente se guarda una referencia.
En Rust, insert recibe la clave y el valor por valor, y el HashMap pasa a ser propietario de lo que almacena. Si un argumento implementa Copy, se guarda una copia y la variable original sigue siendo válida; en caso contrario, se mueve.
Observa lo que pasa aquí:
use std::collections::HashMap;
fn main() {
let clave = String::from("Color Favorito");
let valor = String::from("Azul");
let mut mapa = HashMap::new();
// Al insertar, las variables se MUEVEN dentro del mapa
mapa.insert(clave, valor);
// println!("{}", clave); // ❌ ¡ERROR! 'clave' ya no es válida.
// println!("{}", valor); // ❌ ¡ERROR! 'valor' ya no es válido.
}Las variables clave y valor han sido movidas al interior del mapa. El mapa es ahora responsable de liberarlas cuando él mismo sea destruido.
Si quieres seguir usando la clave después de insertarla, tienes que clonarla al insertarla: mapa.insert(clave.clone(), valor).
Si la clave fuera un tipo simple (i32), se copiaría automáticamente y no habría problema.
Actualizar un HashMap
Modificar datos en un mapa es una operación muy común, y Rust tiene una API muy elegante llamada Entry API para gestionar los casos de “si existe actualiza, si no existe crea”.
Sobrescribir un valor
Si insertas una clave que ya existe, el valor antiguo se reemplaza. insert devuelve ese valor dentro de un Option<V>; si ignoramos el resultado, se destruirá.
mapa.insert(String::from("Azul"), 10);
mapa.insert(String::from("Azul"), 25); // El 10 se pierde, ahora es 25Insertar solo si no existe
A veces queremos poner un valor por defecto solo si la clave está vacía.
mapa.entry(String::from("Amarillo")).or_insert(50);
mapa.entry(String::from("Azul")).or_insert(50); // No hace nada, "Azul" ya es 25El método .entry() devuelve un enum especial que representa el hueco donde podría estar el valor. .or_insert() inserta el valor solo si ese hueco está vacío y, lo más importante, devuelve una referencia mutable al valor (nuevo o viejo).
Actualizar a partir del valor anterior
Imagina que queremos contar cuántas veces aparece cada palabra en un texto.
let texto = "hola mundo maravilloso mundo";
let mut contador = HashMap::new();
for palabra in texto.split_whitespace() {
// 1. entry busca la clave
// 2. or_insert(0) asegura que exista un valor (empieza en 0 si es nueva)
// 3. Devuelve una referencia mutable (&mut i32) al valor
let cuenta = contador.entry(palabra).or_insert(0);
// 4. Desreferenciamos y sumamos 1
*cuenta += 1;
}
println!("{:?}", contador);
// {"mundo": 2, "hola": 1, "maravilloso": 1}La API entry permite resolver la búsqueda y la posible inserción sin repetir manualmente ambas operaciones.
El algoritmo de hashing
El HashMap de la biblioteca estándar usa por defecto un hasher con claves aleatorias para ofrecer resistencia frente a ataques HashDoS. El algoritmo concreto no forma parte de la API estable y puede cambiar entre versiones.
Para la inmensa mayoría de aplicaciones, el valor predeterminado es una elección razonable. Si has medido que el hashing es un cuello de botella, puedes proporcionar otra implementación de BuildHasher, valorando también sus propiedades de seguridad.