Iteradores en Rust
Un iterador es cualquier cosa capaz de producir una secuencia de valores, uno por uno. Los vienes usando desde la página de bucles for, aunque no los hayas llamado así: todo bucle for que recorre una colección funciona gracias a un iterador que trabaja por detrás.
Obtener un iterador
Sección titulada «Obtener un iterador»fn main() { let numbers = vec![1, 2, 3]; let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(1) println!("{:?}", iter.next()); // Some(2) println!("{:?}", iter.next()); // Some(3) println!("{:?}", iter.next()); // None}Salida:
Some(1)Some(2)Some(3)None.next() devuelve un Option: Some(valor) por cada elemento y None cuando ya no quedan más. En realidad, un bucle for no hace otra cosa que llamar a .next() una y otra vez, por debajo, hasta recibir None.
map: transformar cada elemento
Sección titulada «map: transformar cada elemento».map() recibe un closure (una función anónima) y lo aplica a cada elemento, produciendo un iterador nuevo:
fn main() { let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);}Salida:
[2, 4, 6, 8, 10].collect() es lo que convierte el iterador de nuevo en una colección concreta (aquí, un Vec<i32>). Sin él, solo tendrías un iterador sin evaluar, ahí parado sin hacer nada.
filter: quedarte solo con algunos elementos
Sección titulada «filter: quedarte solo con algunos elementos»fn main() { let numbers = vec![1, 2, 3, 4, 5, 6]; let evens: Vec<&i32> = numbers.iter().filter(|n| **n % 2 == 0).collect();
println!("{:?}", evens);}Salida:
[2, 4, 6]El doble **n parece raro. Pasa que .filter() le da al closure una referencia a una referencia (&&i32), así que hay que desreferenciar dos veces para compararlo con un número normal. Es un pequeño detalle muy común; no te preocupes todavía por la explicación completa.
Encadenar map y filter
Sección titulada «Encadenar map y filter»Aquí es donde encadenar métodos de iteradores empieza a valer la pena: puedes combinar varios pasos en un solo flujo fácil de leer:
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers .iter() .filter(|n| **n % 2 == 0) // quedarse con los pares .map(|n| n * 10) // multiplicar cada uno por 10 .collect();
println!("{:?}", result);}Salida:
[20, 40, 60]Compáralo con escribir la misma lógica con un bucle for a mano y un Vec vacío al que vas añadiendo elementos. No está mal, pero la versión encadenada se lee casi como “filtra, luego multiplica, luego recoge”, que es justo la secuencia de pasos que quieres describir.
Otros métodos habituales
Sección titulada «Otros métodos habituales»fn main() { let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum(); let count = numbers.iter().count(); let has_even = numbers.iter().any(|n| n % 2 == 0);
println!("suma: {total}, cantidad: {count}, ¿hay algún par?: {has_even}");}Salida:
suma: 15, cantidad: 5, ¿hay algún par?: true.sum() lo suma todo, .count() te dice cuántos elementos hay y .any() responde a “¿cumple la condición al menos un elemento?”. Son solo algunos de los métodos que tiene todo iterador. Hay muchos más, como enumerate, zip, take y rev. En la página de Iteradores personalizados crearás tu propio iterador y verás que recibe todos estos métodos gratis.
Práctica
Sección titulada «Práctica»1. Suma de los cuadrados impares
Sección titulada «1. Suma de los cuadrados impares»Toma los números del 1 al 10, quédate solo con los impares, eleva cada uno al cuadrado y súmalos todos. Hazlo en una sola cadena de iteradores, sin ninguna variable mut. Pista: encadena .filter(), .map() y .sum(), y dale un tipo al resultado, como let total: i32.
Ver solución
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total: i32 = numbers .iter() .filter(|n| **n % 2 == 1) // se queda con 1, 3, 5, 7, 9 .map(|n| n * n) // 1, 9, 25, 49, 81 .sum();
println!("suma de los cuadrados impares: {total}");}Salida:
suma de los cuadrados impares: 165Rust necesita el : i32 en total porque .sum() puede sumar en varios tipos de número distintos, y no puede adivinar cuál quieres.
2. Grita las palabras largas
Sección titulada «2. Grita las palabras largas»Empieza con vec!["the", "quick", "brown", "fox", "jumps"]. Construye un nuevo Vec<String> que tenga solo las palabras de más de 3 letras, en mayúsculas, e imprímelo con {:?}. Pista: .filter(|w| w.len() > 3) se queda con las palabras largas, y .to_uppercase() convierte un &str en un String en mayúsculas.
Dejamos las palabras en inglés a propósito: .len() cuenta bytes, no letras, y una palabra con tilde (como “marrón”) ocuparía más bytes de los que parece.
Ver solución
fn main() { let words = vec!["the", "quick", "brown", "fox", "jumps"];
let loud: Vec<String> = words .iter() .filter(|w| w.len() > 3) .map(|w| w.to_uppercase()) .collect();
println!("{:?}", loud);}Salida:
["QUICK", "BROWN", "JUMPS"]words no cambia. .iter() solo lo toma prestado, así que puedes seguir usando words después de la cadena.
Resumen
Sección titulada «Resumen»- Un iterador produce valores uno por uno mediante
.next(), que devuelve unOption. - Los bucles
forusan iteradores por debajo. .map()transforma cada elemento;.filter()se queda solo con los que cumplen una condición;.collect()convierte el resultado de nuevo en una colección concreta.- Encadenar métodos de iteradores suele ser más claro que un bucle a mano con un acumulador mutable.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué devuelve .next() en un iterador?
2. ¿Qué hace .collect() al final de una cadena como .iter().map(...).collect()?
3. En .filter(|n| **n % 2 == 0), ¿por qué se desreferencia n dos veces?
4. ¿Qué hace funcionar a un bucle for cuando recorre una colección?
5. ¿Con qué elementos se queda .filter() en el iterador resultante?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- Procesar una serie de elementos con iteradores — El libro de Rust (en inglés): la introducción oficial a
next,map,filterycollect, con más ejemplos. - Comparar el rendimiento: bucles frente a iteradores — El libro de Rust (en inglés): por qué una cadena de iteradores es igual de rápida que un bucle escrito a mano.
- El trait
Iterator— documentación de la biblioteca estándar (en inglés): la lista completa de métodos de los iteradores, cada uno con un pequeño ejemplo. Ideal para echarle un vistazo. - Iterators — Rust by Example (en inglés): un ejemplo corto y ejecutable que construye un iterador de Fibonacci.