El trait Iterator y next: producir valores uno a uno
Un iterador es un objeto que produce una secuencia de valores bajo demanda. El trait `Iterator`, su tipo asociado `Item` y su único método obligatorio `next`, que devuelve `Some` mientras quedan valores y `None` al agotarse. Implementarlo a mano y recibir gratis toda la maquinaria de adaptadores.
Un bucle recorre; un iterador produce. La diferencia es honda: en lugar de escribir a mano el índice, la condición de parada y el avance, delegas la pregunta “¿cuál es el siguiente valor?” a un objeto que sabe responderla. Ese objeto es un iterador, y toda la potencia de este nivel —los map, los filter, los collect— descansa sobre un único método diminuto: next. Comprender el iterador es descubrir que una secuencia, por larga o infinita que sea, se reduce a una sola promesa: entregar un valor más cuando alguien lo pida, o admitir con honestidad que ya no queda ninguno.
- Definir qué es un iterador: un productor de valores bajo demanda.
- Leer el trait
Iterator, su tipo asociadoItemy su métodonext. - Conducir un iterador a mano llamando a
nexthasta agotarlo. - Implementar
Iteratorpara un tipo propio y heredar gratis sus adaptadores.
Qué es un iterador
Un iterador es un valor que encapsula el estado necesario para recorrer una secuencia y sabe entregar sus elementos de uno en uno, bajo demanda. No contiene forzosamente los datos —un rango 0..1_000_000 es un iterador que no guarda un millón de números, solo el actual y el límite—; contiene la receta para producir el siguiente cuando se lo pidan.
Esa idea —producir bajo demanda en vez de materializar de golpe— es lo que separa un iterador de una colección. Un Vec es los datos; un iterador es un cursor con la lógica para avanzar sobre ellos, o incluso para fabricarlos al vuelo. La colección ocupa memoria proporcional a su tamaño; el iterador, casi siempre, ocupa un puñado de bytes constante sin importar cuántos valores acabe produciendo.
Colección
Es los datos: un Vec, un HashMap. Ocupa memoria proporcional a su tamaño y persiste hasta que la sueltas.
Iterador
Sabe pedir el siguiente dato. Guarda solo su estado —unos bytes— y es efímero: se gasta al recorrerlo una vez.
El trait Iterator: Item y next
En Rust, “ser un iterador” significa una cosa muy concreta: implementar el trait Iterator. Su definición, despojada de adornos, es sorprendentemente pequeña:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...decenas de metodos mas, todos con implementacion por defecto
}
Dos piezas cargan con todo el peso. type Item es un tipo asociado: declara qué clase de valor produce este iterador —i32, &str, char, lo que sea—. Y next es el único método obligatorio: cada vez que lo llamas, avanza el estado interno y devuelve Some(valor) con el siguiente elemento, o None cuando la secuencia se ha agotado.
Fíjate en el &mut self: llamar a next muta el iterador, porque avanzar consume su estado. Un iterador es, literalmente, una máquina de estados: cada next la hace transitar de una posición a la siguiente. Por eso, para recorrer algo, necesitas un binding mutable.
Conducir next a mano
Antes de dejar que los bucles y los adaptadores lo hagan por ti, vale la pena tirar del iterador con tus propias manos. Toda colección estándar te da uno con iter:
let v = vec![10, 20, 30];
let mut it = v.iter(); // it produce elementos de tipo &i32
assert_eq!(it.next(), Some(&10));
assert_eq!(it.next(), Some(&20));
assert_eq!(it.next(), Some(&30));
assert_eq!(it.next(), None); // agotado: no quedan valores
assert_eq!(it.next(), None); // y sigue devolviendo None
Cada next entrega el siguiente valor envuelto en Some; cuando no queda ninguno, devuelve None, y por convención lo seguirá haciendo en todas las llamadas posteriores. El None es la señal universal de fin de secuencia: no hay excepciones ni centinelas mágicos; la ausencia se codifica, como todo en Rust, en el sistema de tipos vía Option.
Un bucle for no es más que esto automatizado: pide next una y otra vez, liga cada Some a la variable del bucle y se detiene en el primer None. Lo veremos con detalle al final del nivel; por ahora basta saber que for habla con next.
Un iterador propio
Como next es lo único obligatorio, implementar Iterator para un tipo propio se reduce a escribir esa función y nada más. Definamos un contador que va de un valor a otro:
struct Contador { actual: u32, hasta: u32 }
impl Iterator for Contador {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.actual < self.hasta {
let n = self.actual;
self.actual += 1; // avanza el estado
Some(n)
} else {
None // agotado
}
}
}
Con esas líneas, Contador es un ciudadano de pleno derecho del mundo de los iteradores. Y aquí llega la recompensa desproporcionada: al implementar un método, heredas cientos. Todos los adaptadores y consumidores del trait —map, filter, sum, collect, zip, count…— tienen implementación por defecto escrita en términos de next, así que aparecen gratis sobre tu tipo:
let suma: u32 = Contador { actual: 0, hasta: 4 }.sum(); // 0+1+2+3 = 6
let cuadrados: Vec<u32> = Contador { actual: 1, hasta: 4 }
.map(|n| n * n)
.collect(); // [1, 4, 9]
No escribiste sum, map ni collect; los recibiste por implementar next. Esa es la economía del diseño: un método fundamental, un océano de comportamiento derivado.
El trait Iterator es el ejemplo canónico de un patrón que ya viste con las implementaciones por defecto: define un núcleo mínimo —next— y construye todo lo demás encima. map, filter, take, sum, count, find y el resto son métodos con cuerpo por defecto que solo llaman a next. Por eso implementar un iterador cuesta tan poco y rinde tanto: cada tipo que sabe decir “mi siguiente valor” obtiene, sin más trabajo, toda la biblioteca funcional de Rust.
flowchart LR llamada[Se llama a next] --> mira[Consulta el estado interno] mira --> quedan[Aun quedan valores] mira --> agotado[Ya no quedan] quedan --> some[Devuelve Some con el valor] some --> avanza[Avanza el estado interno] avanza --> llamada agotado --> none[Devuelve None] none --> fin[La iteracion termina] style some fill:#a6e3a1,color:#11111b style none fill:#f38ba8,color:#11111b style fin fill:#cba6f7,color:#11111b
Detente en lo que next consigue. Una secuencia —una idea que asociamos con “muchos valores a la vez”, una lista tendida en memoria— queda representada por un objeto que en ningún momento contiene la secuencia entera, sino solo el estado suficiente para responder una pregunta: ¿cuál es el siguiente, si es que hay alguno? Toda la noción de “recorrer una colección” se comprime en esa función de firma fn next(&mut self) -> Option<Self::Item>, y esa compresión es la que hace posible lo demás. Como el iterador no materializa nada, puede representar lo infinito —0.. produce naturales sin fin sin ocupar memoria sin fin— y puede fabricar valores que no existían —un iterador que lee líneas de un fichero las produce a medida que las pide, no antes—. El Option del retorno no es un detalle: es la codificación honesta del final, la frontera entre “hay más” y “se acabó” convertida en tipo, sin centinelas ni longitudes que llevar aparte. Y el &mut self revela la naturaleza profunda: un iterador es una máquina de estados cuyo alfabeto de salida es Self::Item y cuya única transición es next. Sobre esa máquina minúscula, Rust erige toda un álgebra de composición —los adaptadores del resto del nivel— con la garantía de que cada pieza habla el mismo idioma diminuto. La lección trasciende a Rust: la forma más poderosa de representar una secuencia no es tenerla, sino saber pedir el siguiente elemento. El iterador es esa pregunta, cristalizada en un trait, y su modestia aparente —un método, un tipo asociado— es exactamente la fuente de su poder.
Recorrer un iterador lo consume: una vez que next ha devuelto None, la máquina de estados llegó a su fin y no se rebobina. Si necesitas recorrer los mismos datos otra vez, pide un iterador nuevo (v.iter() de nuevo) o clónalo antes de agotarlo. No confundas el iterador —el cursor, efímero— con la colección —los datos, persistentes—: v.iter() no consume el Vec, pero el iterador que devuelve sí se gasta al recorrerlo.
- Crea
let mut it = [1, 2, 3].iter();y llama anextcuatro veces, comprobando conassert_eq!los tresSomey elNonefinal. Explica por quéitdebe sermut. - Implementa
struct Fibonacci { a: u64, b: u64 }con unimpl Iteratorcuyonextdevuelva siempreSomedel siguiente término. Tómalo con.take(10).collect::<Vec<_>>()y observa que un iterador infinito es perfectamente válido. - Sobre tu
Contador, encadena.map(|n| n * 2).sum::<u32>()sin haber escrito nimapnisum, y razona de dónde salen esos métodos. - Explica con tus palabras por qué
nextrecibe&mut selfy qué significaría, conceptualmente, unnextque recibiera solo&self. - Describe la diferencia entre la colección
vec![1, 2, 3]y el iteradorvec![1, 2, 3].into_iter()en términos de qué guarda cada uno y cuánto vive.