Lección 15 de 26 Colecciones y errores
HashMap: pares clave-valor
- Un HashMap guarda pares clave → valor. Para encontrar un valor no usas una posición, sino su clave.
- insert añade pares: la clave "Ana" guarda un 10 y la clave "Luis", un 7.
- get busca por clave y devuelve un Option: Some(&10) si la clave existe y None si no.
- entry(...).or_insert(0) crea la clave con un 0 si no existe, y luego le sumas 1. ¡Ideal para contar!
Un vector guarda los valores por posición: el primero, el segundo, el tercero… Pero muchas veces quieres buscarlos por un nombre: la puntuación de «Ana», el precio de «manzanas», la capital de «Francia». Para eso está HashMap<K, V>, que asocia cada clave (de tipo K) con un valor (de tipo V).
Es lo que en otros lenguajes se llama diccionario, mapa u objeto. Buscar por clave es muy rápido, aunque el mapa tenga millones de entradas.
Crear un HashMap e insertar
HashMap no está disponible por defecto como Vec o String: tienes que importarlo con use. Después lo creas con HashMap::new() y añades pares con insert:
use std::collections::HashMap;
fn main() {
let mut puntos = HashMap::new();
puntos.insert(String::from("Ana"), 10);
puntos.insert(String::from("Luis"), 7);
println!("Jugadores: {}", puntos.len());
}Todas las claves tienen que ser del mismo tipo y todos los valores también. Aquí Rust deduce que puntos es un HashMap<String, i32> a partir del primer insert.
Leer valores: get
get busca una clave y devuelve un Option<&V>: Some con una referencia al valor si la clave existe, o None si no:
use std::collections::HashMap;
fn main() {
let mut puntos = HashMap::new();
puntos.insert(String::from("Ana"), 10);
puntos.insert(String::from("Luis"), 7);
match puntos.get("Ana") {
Some(p) => println!("Ana tiene {p} puntos"),
None => println!("Ana no está jugando"),
}
println!("¿Está Eva? {}", puntos.contains_key("Eva"));
let quitado = puntos.remove("Luis");
println!("He quitado a Luis: {quitado:?}");
println!("Quedan {}", puntos.len());
}contains_keyresponde con unboolsi la clave existe.removequita la entrada y te devuelve su valor en unOption(Nonesi no existía).- Aunque las claves sean
String, puedes buscar con un&strcomo"Ana".
También puedes leer con corchetes, puntos["Ana"], pero si la clave no existe el programa hace panic. Úsalo solo cuando estés seguro de que está.
Recorrer un HashMap
Con for (clave, valor) in &mapa recorres todos los pares:
use std::collections::HashMap;
fn main() {
let mut capitales = HashMap::new();
capitales.insert("España", "Madrid");
capitales.insert("Francia", "París");
capitales.insert("Italia", "Roma");
for (pais, capital) in &capitales {
println!("{pais}: {capital}");
}
}Ejecútalo varias veces y verás que el orden cambia. Un HashMap no guarda las entradas en el orden en que las insertas ni en orden alfabético: las coloca según un cálculo interno (el hash) que, además, cambia en cada ejecución.
Si necesitas un orden concreto, recoge las claves en un vector y ordénalo:
use std::collections::HashMap;
fn main() {
let mut capitales = HashMap::new();
capitales.insert("Italia", "Roma");
capitales.insert("España", "Madrid");
capitales.insert("Francia", "París");
let mut paises: Vec<_> = capitales.keys().collect();
paises.sort();
for pais in paises {
println!("{pais}: {}", capitales[pais]);
}
}keys() recorre solo las claves (values() haría lo mismo con los valores) y collect() las reúne en un vector. El _ de Vec<_> le pide a Rust que deduzca el tipo de los elementos.
HashMap y ownership
Al hacer insert, el mapa se queda con la clave y el valor. Si son Copy, como un i32, se copian; si son String, se mueven y ya no puedes usarlos:
use std::collections::HashMap;
fn main() {
let color = String::from("color");
let azul = String::from("azul");
let mut ajustes = HashMap::new();
ajustes.insert(color, azul);
println!("{color}");
}Es la misma regla de siempre: el HashMap pasa a ser el dueño. Si necesitas seguir usando el texto, inserta un clone().
Actualizar valores
Si haces insert con una clave que ya existe, el valor antiguo se sobrescribe:
use std::collections::HashMap;
fn main() {
let mut stock = HashMap::new();
stock.insert("peras", 4);
stock.insert("peras", 6);
println!("Peras: {}", stock["peras"]);
}Para insertar solo si la clave no existe está entry. Devuelve una «entrada» del mapa, y su método or_insert mete el valor indicado si la clave no estaba. En los dos casos te devuelve una referencia mutable (&mut V) al valor que queda guardado:
use std::collections::HashMap;
fn main() {
let mut stock = HashMap::new();
stock.insert("peras", 4);
stock.entry("peras").or_insert(100); // ya existe: no hace nada
stock.entry("kiwis").or_insert(12); // no existe: la inserta
println!("Peras: {}, kiwis: {}", stock["peras"], stock["kiwis"]);
}El contador de palabras
Esa referencia mutable permite el patrón más famoso de los HashMap: contar cosas.
use std::collections::HashMap;
fn main() {
let texto = "el gato y el perro y el pez";
let mut conteo = HashMap::new();
for palabra in texto.split_whitespace() {
*conteo.entry(palabra).or_insert(0) += 1;
}
println!("el: {}", conteo["el"]);
println!("y: {}", conteo["y"]);
println!("gato: {}", conteo["gato"]);
}Si la palabra es nueva, or_insert(0) la añade con un 0; si ya estaba, la deja como está. En los dos casos devuelve un &mut al contador, y con * le sumas 1:
Con el mismo truco puedes agrupar: mapa.entry(clave).or_insert(Vec::new()).push(valor) crea el vector la primera vez y después va añadiendo elementos.
BTreeMap: claves ordenadas
Si siempre vas a necesitar las claves en orden, usa BTreeMap, también de std::collections. Tiene casi la misma API que HashMap, pero recorre las entradas ordenadas por clave:
use std::collections::BTreeMap;
fn main() {
let mut capitales = BTreeMap::new();
capitales.insert("Italia", "Roma");
capitales.insert("España", "Madrid");
capitales.insert("Francia", "París");
for (pais, capital) in &capitales {
println!("{pais}: {capital}");
}
}HashMap o BTreeMap: Usa
HashMappor defecto, porque suele ser más rápido. EligeBTreeMapcuando necesites recorrer las claves en orden.
Hora de practicar
Ejercicios
0/4 resueltos
Reto 1 de 4 Resuelto
HashMap vacío llamado edades e inserta a "Ana" con 31 años y a "Luis" con 25. Después imprime Ana tiene 31 años consultando el mapa con get y, en otra línea, ¿Está Eva? false usando contains_key.Ana tiene 31 años ¿Está Eva? false
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.