Un iterador es un valor que produce elementos de uno en uno.
Hasta ahora, para recorrer una colección hemos usado el bucle for de toda la vida.
let v = vec![1, 2, 3];
for x in &v {
println!("{}", x);
}Esto funciona genial, pero Rust nos ofrece una abstracción mucho más potente: los Iteradores.
Un iterador es un objeto que es responsable de recorrer una secuencia de elementos y determinar cuándo se ha terminado. En Rust, los iteradores son perezosos (lazy), lo que significa que no hacen absolutamente nada hasta que tú se lo pides explícitamente.
El trait Iterator
En el corazón de todo esto se encuentra el trait Iterator. Todos los iteradores implementan este contrato. Su definición simplificada es así:
pub trait Iterator {
type Item; // El tipo de dato que devuelve
fn next(&mut self) -> Option<Self::Item>;
}Lo único que un objeto necesita para ser un iterador es tener un método next.
- Cada vez que llamas a
next, el iterador te devuelve el siguiente elemento envuelto enSome(valor). - Cuando se acaban los datos, devuelve
None.
Podemos usar esto manualmente (aunque rara vez lo haremos):
fn main() {
let v = vec![1, 2, 3];
// Creamos un iterador explícito
let mut iter = v.iter();
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None); // Fin de la secuencia
}¿Por qué el bucle for funcionaba?
En realidad, el bucle for de Rust es “azúcar sintáctico”. Cuando escribes for x in v, Rust llama automáticamente a .into_iter() y usa .next() repetidamente hasta que recibe None.
Es decir, el bucle for es un consumidor de iteradores.
Tipos de iteradores
Al igual que ocurría al recorrer vectores, tenemos tres formas de crear un iterador dependiendo de qué queramos hacer con los datos:
.iter(): Itera sobre referencias inmutables (&T)..iter_mut(): Itera sobre referencias mutables (&mut T), permitiendo modificar la colección original..into_iter(): Crea un iterador que toma posesión (consume) de la colección y devuelve los valores (T). La colección original deja de ser válida.
Evaluación perezosa
Esta es la clave para entender el rendimiento de Rust.
Los adaptadores de iteradores no recorren los elementos hasta que un consumidor los fuerza a ello. Construir la cadena crea algunos valores adaptadores, normalmente muy ligeros; el trabajo sobre cada elemento no empieza hasta consumirla.
Mira este código:
let v = vec![1, 2, 3];
// Aquí NO se ejecuta nada. Ni un solo cálculo.
// Solo estamos creando una estructura que "sabe" que tiene que sumar 1.
let iterador_transformado = v.iter().map(|x| x + 1);Si compilamos el código de arriba, Rust probablemente nos avisará de que el iterador no se usa. Para que ocurra algo, necesitamos métodos Consumidores.
Métodos consumidores
Son métodos que llaman a next() internamente y “gastan” el iterador para producir un resultado final.
collect()
Es el más común. Transforma el iterador de vuelta en una colección (como un Vec).
let v = vec![1, 2, 3];
// Ahora SÍ se ejecuta el map.
// Necesitamos especificar el tipo Vec<i32> porque collect puede crear muchas cosas.
let v2: Vec<i32> = v.iter().map(|x| x + 1).collect();
println!("{:?}", v2); // [2, 3, 4]Otros consumidores
sum(): Suma todos los elementos.count(): Cuenta los elementos.min()/max(): Busca el mínimo o máximo.for_each(): Ejecuta un closure para cada elemento (efectos secundarios).
let total: i32 = v.iter().sum();