Saltar al contenido

IntoIterator: iter, iter_mut, into_iter y cómo for los usa

El trait `IntoIterator` es el puente que el bucle `for` cruza: `for x in c` se desazucara en `c.into_iter()`. Una colección ofrece tres iteradores —`iter` presta `&T`, `iter_mut` presta `&mut T`, `into_iter` mueve `T`—, y elegir entre ellos es elegir el modo de propiedad. Por qué `for x in v`, `for x in &v` y `for x in &mut v` recorren lo mismo de tres maneras.

Llevas todo el nivel diciendo “el for habla con next”. Ahora se cierra el círculo. Entre una colección y un iterador media un trait, IntoIterator, y el bucle for no es más que azúcar sobre él: for x in coleccion se convierte en pedir coleccion.into_iter() y tirar de next hasta el None. Pero hay una elección escondida en ese puente. Una misma colección ofrece tres maneras de convertirse en iterador —prestar, prestar para mutar o entregarse por completo—, y cuál eliges lo decides con algo tan pequeño como un &. Iterar, resulta, no es una operación: son tres, y las tres son las tres formas de propiedad de Rust proyectadas sobre el recorrido.

  • Leer el trait IntoIterator y ver cómo for se desazucara en into_iter.
  • Distinguir iter, iter_mut e into_iter por el tipo de Item que producen.
  • Elegir entre prestar, mutar y consumir al recorrer una colección.
  • Saber qué hace for x in v, for x in &v y for x in &mut v.

IntoIterator: el puente que for cruza

IntoIterator es el trait de “esto se puede convertir en un iterador”. Su método into_iter toma self y devuelve un iterador:

Rust
pub trait IntoIterator {    type Item;    type IntoIter: Iterator<Item = Self::Item>;    fn into_iter(self) -> Self::IntoIter;}

Y el bucle for es exactamente esto, escrito por ti con menos ruido. Esta línea:

Rust
for x in coleccion {    usar(x);}

es azúcar sintáctico para, en esencia:

Rust
let mut it = coleccion.into_iter();while let Some(x) = it.next() {    usar(x);}

El for no sabe nada de Vec, de HashMap ni de rangos: solo sabe pedir into_iter y llamar a next. Cualquier tipo que implemente IntoIterator es recorrible con for, y por eso el mismo bucle sirve para toda colección de la biblioteca y para las tuyas.

iter, iter_mut, into_iter: prestar, mutar, consumir

Aquí está el corazón del tema. Las colecciones ofrecen tres formas de iterar, que se diferencian solo en el tipo de Item que producen —y ese tipo lo cambia todo—:

Rust
let v = vec![1, 2, 3];v.iter();       // Item = &i32     : presta cada elemento en solo lecturav.iter_mut();   // Item = &mut i32 : presta cada elemento para modificarlov.into_iter();  // Item = i32      : entrega cada elemento moviendolo
  • iter produce referencias compartidas &T: lees, la colección sigue viva e intacta después.
  • iter_mut produce referencias mutables &mut T: modificas en el sitio, la colección sigue siendo tuya.
  • into_iter produce valores T: mueve cada elemento fuera de la colección, que queda consumida.
Rust
let mut v = vec![1, 2, 3];for x in v.iter_mut() {    *x *= 10;                 // x es &mut i32: modifico el elemento real}// v == [10, 20, 30], sigue siendo mialet total: i32 = v.iter().sum();   // la leo sin consumirla

for sobre valor, referencia o referencia mutable

No hace falta llamar a los tres métodos a mano: for los selecciona según cómo le des la colección, porque IntoIterator está implementado para Vec<T>, para &Vec<T> y para &mut Vec<T>, cada uno con un Item distinto:

Rust
// Cada linea ilustra una forma distinta, por separado:for s in &v { }        // s: &String      presta, v sobrevivefor s in &mut v { }    // s: &mut String  muta en el sitio, v debe ser mutfor s in v { }         // s: String       mueve, v se consume aqui

La regla es mecánica y hermosa: &v desemboca en iter, &mut v en iter_mut, y v a secas en into_iter. Ese & no es decorativo: es la palanca con la que eliges entre observar, modificar o consumir. Y el compilador lo respeta hasta el final: tras for s in v, usar v es un error de “valor movido”, porque el bucle se llevó la colección entera.

Rust
let v = vec![1, 2, 3];for x in v { }              // into_iter: v se consume// println!("{v:?}");       // ERROR: v fue movida por el for

Un matiz de la edición: arrays por valor

Desde la edición 2021 —y por tanto asentado en la 2024 que usas—, un array también implementa IntoIterator por valor: for x in [1, 2, 3] entrega i32, no &i32. Si quieres referencias, itera sobre &[1, 2, 3]. Es la misma regla de siempre —el & elige el modo— aplicada por fin también a los arrays.

Rust
for x in [1, 2, 3] { }      // x: i32   (por valor, desde la edicion 2021)for x in &[1, 2, 3] { }     // x: &i32  (prestado)

iter → &T

Presta cada elemento en solo lectura. La colección sobrevive intacta. Lo eliges con for x in &c.

iter_mut → &mut T

Presta cada elemento para modificarlo en el sitio. La colección sigue siendo tuya. Lo eliges con for x in &mut c.

into_iter → T

Mueve cada elemento fuera. La colección se consume y ya no se usa. Lo eliges con for x in c.

Iterar es la propiedad proyectada sobre el recorrido

IntoIterator parece un mero detalle de fontanería —el trait que hace funcionar el for—, pero encierra una de las síntesis más elegantes de Rust. Primero, unifica: el bucle for no conoce ninguna colección en particular, solo el trait, de modo que un único constructo del lenguaje recorre vectores, mapas, conjuntos, rangos, canales y cualquier tipo que alguien defina mañana, sin que el for cambie ni el compilador sepa nada de antemano. Esa es la misma lección de los genéricos y los traits llevada a la sintaxis: el lenguaje se mantiene pequeño y abierto porque delega la capacidad en un contrato. Pero la síntesis honda está en los tres modos. Una colección ofrece iter, iter_mut e into_iter, y no se distinguen por lo que hacen —recorrer— sino por el tipo de su Item: &T, &mut T, T. Y esos tres tipos son, exactamente, los tres modos de acceso sobre los que se construye todo el lenguaje: el préstamo compartido, el préstamo mutable y la posesión. El sistema de propiedad de Rust, que gobierna cada línea que escribes, reaparece aquí intacto, proyectado sobre el acto de recorrer: iterar prestando, iterar mutando, iterar consumiendo. Por eso el & de for x in &v carga tanto significado en tan poco: no adorna, decide si el bucle observará, modificará o devorará la colección, y el borrow checker vela por esa decisión como por cualquier otra —tras un for x in v la colección está movida, y el compilador te lo recuerda—. Y en el otro extremo del nivel, collect con FromIterator es el reflejo de este trait: into_iter deshace una estructura en un flujo, collect cuaja un flujo en una estructura. Entre los dos, todo el procesamiento de secuencias de Rust es un viaje de ida y vuelta entre las colecciones y los flujos perezosos, con la propiedad marcando el camino en cada paso.

Tras consumir con for x in v, la colección ya no existe

for x in v mueve v dentro del bucle: al terminar, la colección ha sido consumida y usarla de nuevo es un error de compilación. Si necesitas la colección después del bucle, itera sobre &v (o &mut v si vas a modificarla) en lugar de sobre v. Es el mismo criterio que aplicas al llamar a una función que toma self frente a una que toma &self: el for no es una excepción a las reglas de propiedad, es una de sus aplicaciones.

Elige el modo de recorrer

  1. Con let mut v = vec![1, 2, 3];, usa for x in &mut v { *x += 100; } y luego imprime v. Explica por qué v debe ser mut y qué tipo tiene x.
  2. Recorre for x in &v sumando en una variable externa y comprueba que después puedes seguir usando v. ¿Qué tipo tiene x?
  3. Escribe for s in v sobre un Vec<String> y luego intenta usar v; lee el error de “valor movido” e identifica qué método invocó el for.
  4. Desazucara a mano for x in 1..=3 en su forma con into_iter y while let Some, y comprueba que hace lo mismo.
  5. Itera for x in [10, 20, 30] y luego for x in &[10, 20, 30]; contrasta el tipo de x en cada caso y relaciónalo con el cambio de la edición 2021.