Aprende Rust

Lección 15 de 26 Colecciones y errores

HashMap: pares clave-valor

  • 4 min de lectura
  • 4 ejercicios
  1. Un HashMap guarda pares clave → valor. Para encontrar un valor no usas una posición, sino su clave.
  2. insert añade pares: la clave "Ana" guarda un 10 y la clave "Luis", un 7.
  3. get busca por clave y devuelve un Option: Some(&10) si la clave existe y None si no.
  4. entry(...).or_insert(0) crea la clave con un 0 si no existe, y luego le sumas 1. ¡Ideal para contar!
1/4

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_key responde con un bool si la clave existe.
  • remove quita la entrada y te devuelve su valor en un Option (None si no existía).
  • Aunque las claves sean String, puedes buscar con un &str como "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 HashMap por defecto, porque suele ser más rápido. Elige BTreeMap cuando necesites recorrer las claves en orden.

Hora de practicar

Ejercicios

0/4 resueltos

Reto 1 de 4

Crea un 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.
Salida esperada
Ana tiene 31 años
¿Está Eva? false
src/main.rs
$ cargo run Listo

Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.

¿Algo no cuadra en este reto? Avísanos