El patrón común de “buscar, verificar si existe y luego insertar o modificar” suele llevar a un código ineficiente en Rust. Si utilizas if map.contains_key(&key) { ... } else { ... }, el HashMap tiene que calcular el valor del hash de la clave y buscarla en la tabla dos veces (una para comprobar la existencia y otra para acceder o insertar). Esto duplica el trabajo de procesamiento innecesariamente.
La Entry API resuelve este problema mediante una aproximación idiomática que garantiza un único lookup (búsqueda) en la tabla. Al llamar al método .entry(key), el HashMap busca la posición de la clave y devuelve un enumerado llamado Entry. Este enum tiene dos variantes: Occupied (la clave ya existe) y Vacant (la clave no existe). Al devolver este objeto, la Entry API te permite trabajar directamente sobre la ubicación de la clave en memoria, ya sea para modificar el valor existente o para insertar uno nuevo.
Existen tres métodos fundamentales para manejar estas variantes:
1. or_insert(valor): Si la entrada está vacía, inserta el valor proporcionado y devuelve una referencia mutable al valor.
2. or_insert_with(|| valor_costoso()): Similar al anterior, pero utiliza una closure para generar el valor. Esto es crucial cuando crear el valor requiere computación pesada o asignación de memoria, ya que la función solo se ejecutará si la entrada realmente es Vacant (evaluación perezosa).
3. and_modify(|v| ...): Permite aplicar una transformación al valor solo si la entrada ya existe (Occupied), devolviendo el mismo objeto Entry para encadenar con un or_insert.
El uso incorrecto de estos patrones (usar contains_key seguido de get_mut) no solo hace el código más verboso, sino que penaliza el rendimiento en aplicaciones de alta carga donde las colisiones de hash o la complejidad de la clave sean significativas.
use std::collections::HashMap;
fn main() {
// Caso 1: El patrón de conteo (frecuencias)
// Usamos and_modify para actualizar y or_insert para inicializar.
let mut frecuencias = HashMap::new();
let palabras = vec!["rust", "es", "genial", "rust", "es", "rust"];
for palabra in palabras {
frecuencias
.entry(palabra.to_string())
.and_modify(|count| *count += 1) // Si existe, incrementa
.or_insert(1); // Si no, inserta 1
}
println!("Frecuencias de palabras: {:?}", frecuencias);
// Caso 2: Agrupamiento de elementos
// Usamos or_insert_with para inicializar un Vec solo si es necesario.
let mut grupos: HashMap<String, Vec<u32>> = HashMap::new();
let numeros = vec![1, 2, 3, 4, 5, 6, 7, 8];
for n in numeros {
let categoria = if n % 2 == 0 { "par" } else { "impar" };
// or_insert_with es eficiente porque Vec::new() solo se llama
// cuando la clave 'categoria' no ha sido encontrada.
grupos
.entry(categoria.to_string())
.or_insert_with(Vec::new)
.push(n);
}
println!("Grupos de números: {:?}", grupos);
// Caso 3: Uso de or_default para tipos que implementan Default
let mut contador_default: HashMap<String, i32> = HashMap::new();
contador_default.entry("puntos".to_string()).or_default();
println!("Con or_default: {:?}", contador_default);
}
Explicación del Código
- En el Caso 1, aplicamos la lógica sobre la variable
frecuencias. El métodoentry(palabra.to_string())realiza la búsqueda única. El método.and_modify(|count| *count += 1)recibe una closure dondecountes una referencia mutable (&mut i32) al valor actual en el mapa; si la clave existía, sumamos uno. Si no existía, el flujo pasa a.or_insert(1), que coloca el valor inicial. - En el Caso 2, trabajamos con
grupos, unHashMap<String, Vec<u32>>. Aquí es vital el uso de.or_insert_with(Vec::new). Si hubiéramos usadoor_insert(Vec::new()), el compilador tendría que instanciar unVecvacío en cada iteración del bucle, incluso si la clave ya estaba presente. Conor_insert_with, elVecsolo se crea mediante la closure cuando la clave esVacant. El método.push(n)se ejecuta sobre el&mut Vec<u32>devuelto poror_insert_with. - En el Caso 3,
contador_defaultutiliza.or_default(). Este método es una forma abreviada de decir “si no existe, usa el valor por defecto de su tipo” (en este caso,0parai32), simplificando aún más el código cuando el tipo de dato implementa el traitDefault.
N° 73