Ir al contenido

Iteradores personalizados en Rust

En la página de iteradores usaste los iteradores que te da la biblioteca estándar: vec.iter(), .map(), .filter(), .sum(). Pero también puedes crear tus propios iteradores. Una cuenta atrás, los números de Fibonacci, las páginas de un informe, las líneas de un formato de archivo propio: cualquier cosa que entregue valores de uno en uno puede ser un iterador.

La buena noticia: solo escribes un método, next. A cambio, Rust te da todos los adaptadores (map, filter, take, sum, zip y decenas más) gratis. Piensa en una máquina de turnos. Tú construyes la parte que imprime el siguiente número. La fila, la pantalla y el cartel de “atendiendo al número…” funcionan solos.

Para que un tipo sea un iterador, implementa el trait Iterator. Necesita dos cosas:

  • type Item: qué tipo de valor entrega el iterador (en traits avanzados tienes más sobre los tipos asociados).
  • fn next(&mut self) -> Option<Self::Item>: devuelve Some(valor) con el siguiente valor, o None cuando ya no quedan más.
struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None // terminado
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
println!("{:?}", countdown.next());
for n in Countdown::new(3) {
println!("{}...", n);
}
println!("¡Despegue!");
}

Salida:

Some(3)
Some(2)
Some(1)
None
None
3...
2...
1...
¡Despegue!
▶ Pruébalo tú mismo

Algunas cosas en las que fijarte:

  • next recibe &mut self porque cambia el iterador. El struct recuerda por dónde va (current), y cada llamada lo hace avanzar. Por eso countdown tiene que ser let mut.
  • Devolver None significa “ya terminé”. Un bucle for llama a next una y otra vez y se detiene en el primer None.
  • Nuestra cuenta atrás sigue devolviendo None después de terminar. Es lo educado, pero el trait no lo exige: algunos iteradores podrían volver a empezar. Si necesitas esa garantía, llama a .fuse() sobre cualquier iterador.

Un iterador no tiene por qué terminar. La sucesión de Fibonacci (0, 1, 1, 2, 3, 5, 8, …) sigue para siempre: cada número es la suma de los dos anteriores. El iterador solo necesita recordar los dos últimos números:

struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.current;
// checked_add devuelve None si hay desbordamiento, y `?` pasa ese None hacia fuera,
// así que el iterador termina en lugar de fallar cuando los números crecen demasiado.
let new_next = self.current.checked_add(self.next)?;
self.current = self.next;
self.next = new_next;
Some(value)
}
}
fn main() {
let first_ten: Vec<u64> = Fibonacci::new().take(10).collect();
println!("{:?}", first_ten);
}

Salida:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ Pruébalo tú mismo

La clave es .take(10). Sin él, collect() intentaría recoger la sucesión entera. Con un iterador infinito, ponle siempre un límite con algo como take, take_while o find.

Esta es la mejor parte. El trait Iterator tiene unos 75 métodos, y todos menos next tienen una implementación por defecto construida sobre next. Así que, en cuanto escribes next, tu tipo puede usarlos todos:

struct Countdown {
current: u32,
}
impl Countdown {
fn new(from: u32) -> Self {
Countdown { current: from }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.current == 0 {
None
} else {
let value = self.current;
self.current -= 1;
Some(value)
}
}
}
fn main() {
let total: u32 = Countdown::new(5).sum();
println!("Suma: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect();
println!("Pares: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect();
println!("Etiquetas: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0);
println!("Primer múltiplo de 7: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect();
println!("Emparejados: {:?}", pairs);
}

Salida:

Suma: 15
Pares: [10, 8, 6, 4, 2]
Etiquetas: ["T-3", "T-2", "T-1"]
Primer múltiplo de 7: Some(98)
Emparejados: [(3, 'a'), (2, 'b'), (1, 'c')]
▶ Pruébalo tú mismo

Es un gran ejemplo de por qué los traits de Rust son tan útiles. Metes un método pequeño y sacas una caja de herramientas enorme.

Los adaptadores como map y take no hacen nada por sí solos. Solo arman un “plan”. El trabajo ocurre únicamente cuando algo consume el iterador: un bucle for, collect, sum, etc. Vamos a demostrarlo con un iterador que imprime algo cada vez que se llama a next:

struct Noisy {
n: u32,
}
impl Iterator for Noisy {
type Item = u32;
fn next(&mut self) -> Option<u32> {
self.n += 1;
println!(" se llamó a next(), devuelve {}", self.n);
Some(self.n)
}
}
fn main() {
println!("Armando la cadena...");
let chain = Noisy { n: 0 }.map(|x| x * 10).take(3);
println!("Todavía no se ha ejecutado nada. Ahora a consumir:");
for value in chain {
println!("recibido {}", value);
}
println!("Listo");
}

Salida:

Armando la cadena...
Todavía no se ha ejecutado nada. Ahora a consumir:
se llamó a next(), devuelve 1
recibido 10
se llamó a next(), devuelve 2
recibido 20
se llamó a next(), devuelve 3
recibido 30
Listo
▶ Pruébalo tú mismo

Aquí hay dos lecciones. Primero, armar la cadena no imprimió nada: ninguna llamada a next(). Segundo, los valores pasan de uno en uno: un next, un map, un recibido, y luego el siguiente valor. No hay ninguna lista escondida en medio. Y Noisy nunca termina, pero el programa sí: después de tres elementos, take(3) simplemente deja de pedir.

Hasta ahora, nuestros structs eran iteradores. Pero muchas veces tienes una colección (un struct que guarda elementos) y quieres que se pueda recorrer, igual que se recorre un Vec. Para eso implementas IntoIterator. Es el trait que usa un bucle for por detrás: for x in cosa en realidad llama a cosa.into_iter().

Las colecciones suelen ofrecer tres formas de recorrerlas, y ayuda verlas en tu propio tipo:

Escribes Método que hay detrás Cada elemento es La colección después
for s in &playlist playlist.iter() &String (solo lectura) se puede seguir usando
for s in &mut playlist playlist.iter_mut() &mut String (se puede cambiar) se puede seguir usando
for s in playlist playlist.into_iter() String (con ownership) movida, ya no existe

Aquí tienes una Playlist que admite las tres. Nuestro struct guarda sus canciones en un Vec, así que podemos entregar los iteradores del propio Vec en lugar de escribir otros nuevos:

struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn iter(&self) -> std::slice::Iter<'_, String> {
self.songs.iter()
}
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> {
self.songs.iter_mut()
}
}
// Hace que funcione `for song in &playlist`
impl<'a> IntoIterator for &'a Playlist {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
// Hace que funcione `for song in &mut playlist`
impl<'a> IntoIterator for &'a mut Playlist {
type Item = &'a mut String;
type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.iter_mut()
}
}
// Hace que funcione `for song in playlist` (y consume la playlist)
impl IntoIterator for Playlist {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.songs.into_iter()
}
}
fn main() {
let mut playlist = Playlist {
songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")],
};
for song in &playlist {
println!("Leyendo: {}", song);
}
for song in &mut playlist {
*song = song.to_uppercase();
}
println!("La más larga: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist {
println!("Tomando: {}", song);
}
// El último bucle movió `playlist`, así que aquí ya no se puede usar.
}

Salida:

Leyendo: intro
Leyendo: main theme
Leyendo: outro
La más larga: Some("MAIN THEME")
Tomando: INTRO
Tomando: MAIN THEME
Tomando: OUTRO
▶ Pruébalo tú mismo

Los nombres de las canciones se quedan en inglés a propósito: max_by_key(|s| s.len()) compara longitudes en bytes, y las letras con tilde ocupan más de un byte, lo que podría cambiar cuál sale como “la más larga”.

Son muchas líneas, pero cada impl tiene la misma forma: dices qué es Item, dices qué tipo de iterador devuelves (IntoIter) y lo devuelves. El 'a es un lifetime (tiempo de vida): dice “los elementos prestados viven tanto como la playlist prestada”. Puedes copiar este patrón para casi cualquier colección que envuelva un Vec.

Vale la pena conocer dos traits más. No los necesitarás a menudo, pero adaptadores como .rev() y .len() dependen de ellos:

  • DoubleEndedIterator añade next_back, que toma elementos desde el final. Desbloquea .rev().
  • ExactSizeIterator promete que el iterador sabe exactamente cuántos elementos le quedan. Desbloquea .len(). Además, en Iterator das un size_hint exacto.
// Produce low, low+1, ..., high-1 (como el rango low..high)
struct Between {
low: u32,
high: u32,
}
impl Iterator for Between {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
let value = self.low;
self.low += 1;
Some(value)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.high.saturating_sub(self.low) as usize;
(left, Some(left))
}
}
impl DoubleEndedIterator for Between {
fn next_back(&mut self) -> Option<u32> {
if self.low >= self.high {
return None;
}
self.high -= 1;
Some(self.high)
}
}
impl ExactSizeIterator for Between {}
fn main() {
let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect();
println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 };
println!("delante {:?}, detrás {:?}", both_ends.next(), both_ends.next_back());
println!("quedan {}", both_ends.len());
}

Salida:

[5, 4, 3, 2, 1]
delante Some(1), detrás Some(5)
quedan 3
▶ Pruébalo tú mismo

Los dos extremos avanzan el uno hacia el otro, y el iterador termina cuando se encuentran. impl ExactSizeIterator for Between {} está vacío: len() usa el size_hint que ya escribiste.

Escribir un struct y un impl es ideal para iteradores que vas a reutilizar. Para uno rápido y de un solo uso, la biblioteca estándar tiene dos ayudantes en std::iter:

  • iter::from_fn(closure): llama a tu closure cada vez que hace falta un valor. El closure devuelve Some(valor) o None, igual que next.
  • iter::successors(first, closure): empieza con un primer valor, y el closure calcula cada valor siguiente a partir del anterior. Devolver None lo termina.
use std::iter;
fn main() {
// from_fn: el closure guarda su propio estado (count)
let mut count = 3;
let countdown: Vec<u32> = iter::from_fn(|| {
if count == 0 {
None
} else {
count -= 1;
Some(count + 1)
}
})
.collect();
println!("{:?}", countdown);
// successors: cada valor se calcula a partir del anterior
let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect();
println!("{:?}", powers);
// Fibonacci en una línea: lleva los dos últimos números como una pareja
let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b)))
.map(|(a, _)| a)
.take(10)
.collect();
println!("{:?}", fib);
}

Salida:

[3, 2, 1]
[1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
▶ Pruébalo tú mismo

powers se detiene solo: la siguiente potencia de diez, 10.000.000.000, no cabe en un u32, así que checked_mul devuelve None y el iterador termina. Una buena regla: usa estos atajos dentro de una sola función, y escribe un struct con nombre cuando otro código necesite crear el iterador o cuando tenga mucho estado.

Olvidar mut al llamar tú mismo a next. next cambia el iterador, así que la variable tiene que ser mutable:

let countdown = Countdown { current: 3 };
println!("{:?}", countdown.next());
error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable
--> src/main.rs:20:22
|
20 | println!("{:?}", countdown.next());
| ^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
19 | let mut countdown = Countdown { current: 3 };
| +++

El compilador dice que no puede tomar countdown prestada como mutable porque no se declaró como mutable. Añade mut. (Un bucle for no lo necesita: toma el iterador y se encarga de eso por ti).

Olvidar type Item. El trait necesita saber qué entregas:

error[E0046]: not all trait items implemented, missing: `Item`
--> src/main.rs:5:1
|
5 | impl Iterator for Countdown {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation
|
= help: implement the missing item: `type Item = /* Type */;`

El mensaje dice que falta el elemento Item en la implementación. Añade type Item = u32; (o lo que produzca tu iterador) dentro del impl.

Recoger un iterador infinito. Fibonacci::new().collect::<Vec<_>>() (sin take) compila sin problemas, pero nunca termina de una forma útil: se ejecuta hasta que los números se desbordan, o para siempre si tu next nunca devuelve None. El compilador no puede detectarlo. Cuando un iterador pueda ser infinito, añade siempre take, take_while o find antes de consumirlo.

  • Implementa Iterator con type Item y fn next(&mut self) -> Option<Self::Item>. Devuelve None cuando termines.
  • En cuanto existe next, obtienes gratis map, filter, take, sum, zip y decenas más.
  • Los iteradores son perezosos (lazy): no pasa nada hasta que un bucle for, collect, sum, etc. los consume, y los elementos pasan de uno en uno.
  • Implementa IntoIterator para &MiTipo, &mut MiTipo y MiTipo para admitir bucles for que toman prestado, toman prestado de forma mutable o consumen.
  • DoubleEndedIterator habilita .rev(); ExactSizeIterator habilita .len().
  • Para iteradores rápidos de un solo uso, usa std::iter::from_fn o std::iter::successors.

Comprueba lo aprendido

1. ¿Qué debería devolver next cuando al iterador ya no le quedan elementos?

2. Si solo implementas next, ¿puedes llamar a .map() y .sum() sobre tu iterador?

3. En el ejemplo de evaluación perezosa, ¿cuándo se llama a next() por primera vez?

4. ¿Qué impl hace que funcione for song in &playlist?

5. ¿Qué trait necesitas para que .rev() funcione con tu iterador?

Puntuación: 0 / 5