Aprende Rust → Lección 6
Con dos colecciones se cubre casi todo lo que se escribe a diario: el Vec, que es una lista que crece, y el HashMap, que asocia claves con valores. Pero lo que de verdad define cómo se programa en Rust no son ellas, sino los iteradores: una forma de describir qué quieres obtener de una colección en lugar de escribir el bucle que lo recorre. Esta lección va de las dos cosas, en ese orden.
Vec: la lista que crece
Un Vec<T> guarda muchos valores del mismo tipo, uno detrás de otro, y puede crecer durante la ejecución. Por dentro son tres datos y un bloque de memoria aparte:
En el día a día esa estructura es invisible y se trabaja con un puñado de métodos:
fn main() {
let mut notas: Vec<i32> = Vec::new();
notas.push(90);
notas.push(75);
notas.push(88);
let dias = vec!["lunes", "martes", "miércoles"];
println!("{} notas, {} días", notas.len(), dias.len());
println!("la primera nota es {}", notas[0]);
println!("la última fue {:?}", notas.pop());
println!("¿hay una décima nota? {:?}", notas.get(9));
}
La macro vec! es el atajo para crear uno con valores ya dentro. Y hay dos formas de leer un elemento que conviene no confundir, porque se comportan de manera muy distinta cuando el índice no existe:
notas[9] frente a notas.get(9). Con los corchetes, un índice fuera de rango detiene el programa con index out of bounds. Con get obtienes un Option: Some si existe y None si no, y decides tú qué hacer. Usa los corchetes solo cuando el índice venga de un sitio en el que ya sabes que es válido, como un bucle sobre la propia colección.Recorrerlo de tres maneras
Aquí reaparece la idea de propiedad de la guía, y es el punto donde más se tropieza. Un for sobre una colección puede pedir prestados los elementos, pedirlos prestados para modificarlos, o quedárselos:
fn main() {
let mut v = vec![1, 2, 3];
for n in &v {
print!("{n} ");
}
println!();
for n in &mut v {
*n *= 10;
}
println!("{v:?}");
for n in v {
print!("{n} ");
}
println!();
}
Las tres formas tienen nombre propio como métodos: iter() presta cada elemento, iter_mut() los presta para modificarlos y into_iter() consume la colección entera. Escribir for n in &v es lo mismo que for n in v.iter(), solo que más corto.
La tercera forma tiene una consecuencia: después de for n in v, el vector ya no existe, porque el bucle se quedó con él. Si lo intentas usar en la línea siguiente, el programa no compila. Es exactamente la regla de propiedad, aplicada al bucle.
HashMap: asociar claves con valores
El HashMap<K, V> guarda pares. No viene en el preludio, así que hay que importarlo:
use std::collections::HashMap;
fn main() {
let mut stock = HashMap::new();
stock.insert("tornillos", 120);
stock.insert("tuercas", 80);
match stock.get("tornillos") {
Some(cantidad) => println!("hay {cantidad} tornillos"),
None => println!("no tenemos tornillos"),
}
println!("¿hay arandelas? {}", stock.contains_key("arandelas"));
let mut claves: Vec<_> = stock.keys().collect();
claves.sort();
println!("artículos: {claves:?}");
}
Fíjate en que get devuelve un Option, no el valor directamente: la clave puede no estar, y el tipo lo dice. Y fíjate también en que ordeno las claves antes de imprimirlas, porque el orden de recorrido de un HashMap no está garantizado y puede cambiar entre ejecuciones. Si necesitas un orden estable, hay que ordenar al salir o usar un BTreeMap, que mantiene las claves ordenadas.
La API entry, que ahorra la mitad del código
El patrón más frecuente con un mapa es «si la clave ya está haz una cosa, y si no, otra». Escrito a mano son cuatro o cinco líneas con un match; con entry es una:
use std::collections::HashMap;
fn main() {
let texto = "el gato y el perro y el pez";
let mut cuenta: HashMap<&str, i32> = HashMap::new();
for palabra in texto.split_whitespace() {
*cuenta.entry(palabra).or_insert(0) += 1;
}
let mut pares: Vec<_> = cuenta.into_iter().collect();
pares.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (palabra, veces) in pares {
println!("{palabra}: {veces}");
}
}
La línea clave merece leerse despacio. entry(palabra) localiza el hueco de esa clave, or_insert(0) pone un cero si estaba vacío y en cualquier caso devuelve una referencia mutable al valor, y el asterisco de delante sirve para llegar al número a través de esa referencia y sumarle uno. Contar cosas es el ejemplo clásico, pero el mismo patrón sirve para agrupar: con or_insert_with(Vec::new) y un push detrás, se construye un mapa de listas.
Los iteradores, que son el verdadero tema
Un iterador es un valor que sabe entregar elementos de uno en uno. Lo interesante es que se pueden encadenar transformaciones sobre él, y que esas transformaciones no hacen nada hasta que alguien pide el resultado.
Un ejemplo con las piezas en su sitio:
fn main() {
let notas = vec![90, 45, 75, 100, 62, 88];
let suma_aprobados: i32 = notas
.iter()
.filter(|&&n| n >= 70)
.sum();
let subidas: Vec<i32> = notas
.iter()
.map(|n| n + 5)
.collect();
println!("suma de aprobados: {suma_aprobados}");
println!("subidas: {subidas:?}");
println!("cuántos aprobados: {}", notas.iter().filter(|&&n| n >= 70).count());
println!("la mejor: {:?}", notas.iter().max());
println!("¿alguna perfecta? {}", notas.iter().any(|&n| n == 100));
}
Esa escritura reemplaza a los bucles con acumulador que se escriben en otros lenguajes, y tiene dos ventajas concretas: se lee de arriba abajo como una descripción de lo que quieres, y el compilador la traduce a un bucle tan eficiente como el que escribirías a mano.
v.iter().map(|x| println!("{x}")); no imprime una sola línea: construye un iterador y lo tira. El compilador lo avisa con unused `Map` that must be used y la nota «iterators are lazy and do nothing unless consumed»; en ese caso concreto incluso sugiere el arreglo, cambiar map por for_each. Cuando una cadena «no funciona», lo primero que hay que mirar es si falta el consumidor.Las piezas que más se usan
| Adaptadores (perezosos) | Qué hacen |
|---|---|
map(f) |
Transforma cada elemento |
filter(p) |
Deja pasar solo los que cumplen la condición |
enumerate() |
Empareja cada elemento con su índice |
zip(otro) |
Recorre dos colecciones a la vez, en parejas |
rev() |
Lo recorre del final al principio |
take(n), skip(n) |
Se queda con los primeros n, o se salta los primeros n |
filter_map(f) |
Transforma y descarta en un solo paso, quedándose con los Some |
| Consumidores | Qué devuelven |
|---|---|
collect() |
Una colección nueva. Suele necesitar que anotes cuál |
sum(), count() |
Un número |
max(), min() |
Un Option, porque la colección puede estar vacía |
find(p) |
El primero que cumple, como Option |
position(p) |
El índice del primero que cumple, como Option |
any(p), all(p) |
Un bool. Paran en cuanto saben la respuesta |
fold(inicio, f) |
Un único valor, combinando todos. El caso general |
for_each(f) |
Nada: se usa por el efecto, como un bucle |
Combinadas resuelven en una expresión cosas que de otro modo serían un bucle con varias variables sueltas:
fn main() {
let ciudades = vec!["Lima", "Quito", "Bogotá", "Caracas"];
let habitantes = vec![9.7, 2.8, 7.9, 2.9];
for (i, (ciudad, millones)) in ciudades.iter().zip(habitantes.iter()).enumerate() {
println!("{}. {ciudad}: {millones} millones", i + 1);
}
let entradas = vec!["12", "no", "30", "x", "5"];
let numeros: Vec<i32> = entradas
.iter()
.filter_map(|t| t.parse().ok())
.collect();
println!("números válidos: {numeros:?}");
println!("total: {}", numeros.iter().sum::<i32>());
let mas_poblada = ciudades
.iter()
.zip(habitantes.iter())
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap());
println!("la mayor: {mas_poblada:?}");
}
El filter_map de ese ejemplo es un buen resumen de la idea general. parse() devuelve un Result, ok() lo convierte en Option descartando el motivo del fallo, y filter_map se queda solo con los Some y los desempaqueta. Las entradas que no eran números desaparecen sin una sola comprobación escrita a mano, y sin que el programa se detenga por ellas.
En resumen
| Lo que quieres hacer | Qué usar |
|---|---|
| Una lista que crece | Vec<T>, con push y pop |
| Leer un elemento que seguro existe | v[i] |
| Leer uno que puede no existir | v.get(i), que da un Option |
| Asociar claves con valores | HashMap<K, V> |
| Claves siempre en orden | BTreeMap<K, V> |
| Contar o agrupar por clave | entry(k).or_insert(…) |
| Recorrer prestando los elementos | for x in &coleccion |
| Recorrer para modificarlos | for x in &mut coleccion |
| Recorrer quedándote con ellos | for x in coleccion |
| Transformar y recoger en otra colección | .map(…).collect() |
| Quedarte con los que cumplen algo | .filter(…) |
| Convertir descartando los fallos | .filter_map(…) |
La idea que conviene llevarse es que en Rust el bucle explícito es la excepción, no la norma. Casi cualquier recorrido se expresa como una cadena de iteradores, y hacerlo así no es solo cuestión de estilo: al describir el qué en vez del cómo, desaparecen de golpe los índices mal calculados, los acumuladores mal inicializados y los recorridos que se salen del final.
Para verlo explicado

«14. Curso de RUST: VECTORES», del canal Luis Serrano Donaire (14:59). Material de otro canal que recomendamos como complemento, no una producción de decodigo.com. El reproductor solo se carga al pulsar, así que YouTube no recibe nada tuyo hasta entonces. También puedes verlo en YouTube.

«44.- Curso Rust. Hash Maps en Rust: Ownership, SipHash y Optimización Avanzada.», del canal Jesús Conde (23:26). Material de otro canal que recomendamos como complemento, no una producción de decodigo.com. El reproductor solo se carga al pulsar, así que YouTube no recibe nada tuyo hasta entonces. También puedes verlo en YouTube.