wandres.dev
ITERADORES · la pereza y su poder

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.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • Definir qué es un iterador: un productor de valores bajo demanda.
  • Leer el trait Iterator, su tipo asociado Item y su método next.
  • Conducir un iterador a mano llamando a next hasta agotarlo.
  • Implementar Iterator para 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.

ℹ️
Un método obligatorio, cientos regalados

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
Un iterador es una secuencia reducida a una sola pregunta

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.

⚠️
Un iterador agotado es de un solo uso

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.

⚔️ Fabrica y conduce tu propio iterador
  1. Crea let mut it = [1, 2, 3].iter(); y llama a next cuatro veces, comprobando con assert_eq! los tres Some y el None final. Explica por qué it debe ser mut.
  2. Implementa struct Fibonacci { a: u64, b: u64 } con un impl Iterator cuyo next devuelva siempre Some del siguiente término. Tómalo con .take(10).collect::<Vec<_>>() y observa que un iterador infinito es perfectamente válido.
  3. Sobre tu Contador, encadena .map(|n| n * 2).sum::<u32>() sin haber escrito ni map ni sum, y razona de dónde salen esos métodos.
  4. Explica con tus palabras por qué next recibe &mut self y qué significaría, conceptualmente, un next que recibiera solo &self.
  5. Describe la diferencia entre la colección vec![1, 2, 3] y el iterador vec![1, 2, 3].into_iter() en términos de qué guarda cada uno y cuánto vive.