Iterador de Rust

Los iteradores (Iterator) en Rust son una herramienta poderosa y flexible para acceder y operar sobre colecciones (como arreglos, vectores, listas enlazadas, etc.) paso a paso.

Los iteradores de Rust se evalúan de forma perezosa, lo que significa que el iterador en sí no ejecuta operaciones de inmediato, sino que produce valores cuando los necesitas.

Los iteradores te permiten recorrer secuencias de manera declarativa, como los elementos de tipos de colección como arreglos, rebanadas (slices), listas enlazadas, etc.

La idea central detrás de los iteradores es separar el proceso de datos de los datos en sí, haciendo que el código sea más claro, más legible y más fácil de mantener.

En Rust, los iteradores se definen implementando el trait Iterator.

El método de trait más básico es next, que se utiliza para devolver uno a uno el siguiente elemento del iterador, hasta que devuelve None para indicar el final.

Ejemplo

pub trait Iterator {
    type Item;
   
    fn next(&mut self) -> Option<Self::Item>;
   
    // Otros métodos con implementación predeterminada, como map, filter, etc.
}

Los iteradores siguen los siguientes principios:

  • Evaluación perezosa (Laziness): Los iteradores en Rust son perezosos, lo que significa que el iterador en sí no realiza ningún cálculo u operación de inmediato, hasta que solicitas datos explícitamente. Esto hace que los iteradores tengan un buen rendimiento y puedan evitar cálculos innecesarios.

  • Verificación de propiedad y préstamos (Ownership and Borrowing Checks): Los iteradores de Rust cumplen estrictamente las reglas de propiedad y préstamo, evitando carreras de datos y errores de memoria. El tiempo de vida de los iteradores está relacionado con los datos subyacentes, lo que garantiza un acceso seguro a los datos.

  • Llamada encadenada (Chaining): Los iteradores de Rust admiten llamadas encadenadas, es decir, se pueden enlazar múltiples métodos de iterador para realizar operaciones combinadas, lo que hace que el código sea conciso y altamente legible. Por ejemplo, mediante el uso de.map()、.filter()、.collect()métodos como estos, se pueden crear complejas canalizaciones de procesamiento de datos.

  • Gestión eficiente de la memoria (Efficient Memory Management): Los iteradores evitan asignaciones de memoria innecesarias, ya que la mayoría de las operaciones se evalúan de forma perezosa y se realizan directamente durante el recorrido cuando se usan. Esto es especialmente importante para manejar grandes colecciones de datos.

  • Abstracción y generalidad (Abstraction and Generality): Los iteradores de Rust, a través delIteratortrait, implementan la abstracción y la generalidad. Cualquier tipo que implemente elIteratortrait puede usarse como iterador en diferentes contextos. Este diseño mejora la reutilización y la modularidad del código.


Crear un iterador

La forma más común es a través del.iter()、.iter_mut()o.into_iter()método de la colección para crear un iterador:

  • .iter(): Devuelve un iterador de referencias inmutables a los elementos de la colección.
  • .iter_mut(): Devuelve un iterador de referencias mutables a los elementos de la colección.
  • .into_iter(): Transfiere la propiedad de la colección y genera un iterador de valores.

Uso del método iter() para crear un iterador prestado:

let vec = vec![1, 2, 3, 4, 5];
let iter = vec.iter();

Uso del método iter_mut() para crear un iterador de préstamo mutable:

let mut vec = vec![1, 2, 3, 4, 5];
let iter_mut = vec.iter_mut();

Uso del método into_iter() para crear un iterador que toma posesión:

let vec = vec![1, 2, 3, 4, 5];
let into_iter = vec.into_iter();

Ejemplo

let v = vec![1, 2, 3];
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 iteración

Métodos de iterador

Los iteradores de Rust ofrecen una gran variedad de métodos para procesar los elementos de una colección. Algunos de los métodos más comunes incluyen:

  • map(): Aplica la función de transformación dada a cada elemento.
  • filter(): Filtra los elementos de la colección según la condición dada.
  • fold(): Realiza un procesamiento acumulativo sobre los elementos de la colección.
  • skip(): Omite un número específico de elementos.
  • take(): Toma un número específico de elementos.
  • enumerate(): Proporciona un índice para cada elemento.
  • ......

Uso del método map() para transformar cada elemento:

let vec = vec![1, 2, 3, 4, 5];
let squared_vec: Vec<i32> = vec.iter().map(|x| x * x).collect();

Uso del método filter() para filtrar elementos según una condición:

let vec = vec![1, 2, 3, 4, 5];
let filtered_vec: Vec<i32> = vec.into_iter().filter(|&x| x % 2 == 0).collect();

Usar un bucle for para recorrer un iterador

Rust proporciona la sintaxis de bucle for para recorrer los elementos de un iterador, que es una forma de recorrido más concisa e intuitiva.

En el fondo, el bucle for de Rust realmente utiliza iteradores.

let vec = vec![1, 2, 3, 4, 5];
for &num in vec.iter() {
    println!("{}", num);
}

En este bucle, vec.iter() devuelve un iterador, el bucle for recorre ese iterador, asigna cada elemento a la variable num y luego ejecuta el código dentro del cuerpo del bucle.

Adaptadores de consumo

Usan el iterador hasta que se consume por completo.

Los iteradores tienen muchos métodos que pueden consumir el iterador; estos ejecutan la iteración para devolver un resultado final (como una suma, una colección, etc.), y estos métodos consumen el propio iterador.

  • collect(): Convierte un iterador en una colección (como un vector, una HashSet).
  • sum(): Calcula la suma de todos los elementos del iterador.
  • product(): Calcula el producto de todos los elementos del iterador.
  • count(): Devuelve el número de elementos del iterador.

Ejemplo

let v = vec![1, 2, 3];
let sum: i32 = v.iter().sum();
assert_eq!(sum, 6);

Adaptadores

Los adaptadores de iterador permiten cambiar o filtrar el contenido del iterador mediante cadenas de métodos, sin consumirlo inmediatamente.

  • map(): Aplica una función a cada elemento y devuelve un nuevo iterador.
  • filter(): Filtra los elementos que cumplen la condición.
  • take(n): Solo devuelve los primerosnelementos del iterador.
  • skip(n): Omite los primerosnelementos y devuelve un iterador de los elementos restantes.
let v = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
assert_eq!(doubled, vec![2, 4, 6, 8, 10]);

Cadena de iteradores

Se pueden encadenar varios adaptadores de iterador para formar una cadena de iteradores.

Ejemplo

use std::iter::Peekable;

let arr = [1, 2, 3, 4, 5];
let mut iter = arr.into_iter().peekable();
while let Some(val) = iter.next() {
    if val % 2 == 0 {
        continue;
    }
    println!("{}", val);
}

Colector

Usa el método collect para recolectar los elementos de un iterador en algún tipo de colección.

let arr = [1, 2, 3, 4, 5];
let sum: i32 = arr.into_iter().sum();

Evaluación perezosa

Como se mencionó anteriormente, los iteradores de Rust son perezosos; esto significa que métodos como map(), filter(), etc. no ejecutan la operación de inmediato, y solo cuando se llama a un método de consumo como collect() se procesan realmente los datos. Esto hace que el procesamiento con iteradores sea más eficiente, ya que evita cálculos innecesarios.

Iterador personalizado

También puedes implementar el trait Iterator para tus propios tipos, solo necesitas definir el método next() .

Por ejemplo, implementa un iterador simple del 1 al 5:

Ejemplo

struct Counter {
    count: usize,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = usize;
   
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
        if self.count <= 5 {
            Some(self.count)
        } else {
            None
        }
    }
}

let mut counter = Counter::new();
while let Some(num) = counter.next() {
    println!("{}", num);  // Imprime del 1 al 5
}

Iterador paralelo

Si necesitas paralelizar operaciones en un entorno multihilo, el crate rayon proporciona soporte para iteradores paralelos; mediante .par_iter() en lugar de .iter(), puedes acelerar las operaciones de iteración en entornos multihilo.

Iteradores y tiempos de vida

El tiempo de vida de un iterador está relacionado con el tiempo de vida de los elementos que itera. El iterador puede tomar prestados los elementos o tomar posesión de ellos. Esto se controla mediante parámetros de tiempo de vida en la implementación del iterador.

Iteradores y cierres

Los adaptadores de iterador se usan a menudo junto con cierres, que te permiten proporcionar lógica personalizada para las operaciones del iterador.

Iteradores y rendimiento

Los iteradores suelen ser muy eficientes porque permiten que el compilador realice optimizaciones. Por ejemplo, el compilador puede insertar en línea las llamadas a los adaptadores de iterador y aprovechar la característica de evaluación perezosa de los iteradores.

Ejemplo

El siguiente ejemplo demuestra cómo usar un iterador para recorrer un arreglo e imprimir los elementos del arreglo.

Ejemplo

// Función principal
fn main() {
    // Definir un arreglo que contiene enteros
    let numbers = vec![1, 2, 3, 4, 5];

    // Usar un iterador para recorrer el arreglo e imprimir cada elemento
    println!("Iterating through the array:");
    for num in numbers.iter() {
        println!("{}", num);
    }

    // Usar el método map del iterador para elevar al cuadrado cada elemento del arreglo y recopilar el resultado en un nuevo arreglo
    let squared_numbers: Vec<i32> = numbers.iter().map(|x| x * x).collect();

    // Imprimir el arreglo después de elevar al cuadrado
    println!("Squared numbers: {:?}", squared_numbers);
}

En el código anterior, primero definimos un arreglo que contiene enterosnumbers, luego usamos eliter()método para obtener el iterador del arreglo, y a través deforbucle recorremos el iterador e imprimimos cada elemento del arreglo. Luego usamos elmap()método para elevar al cuadrado cada elemento del arreglo, y usamos elcollect()método para recopilar el resultado en un nuevo arreglosquared_numbers. Finalmente, imprimimos el arreglo elevado al cuadrado.

Al ejecutar el programa, se puede ver que se imprime cada elemento del arreglo original, así como el nuevo arreglo después de la operación de elevar al cuadrado:

Iterating through the array:
1
2
3
4
5
Squared numbers: [1, 4, 9, 16, 25]

Este ejemplo demuestra el uso básico de los iteradores en Rust, incluyendo el recorrido, la transformación y la recopilación de resultados.

El siguiente ejemplo utiliza el método filter() para filtrar un array y mostrar el resultado filtrado:

Ejemplo

// Función principal
fn main() {
    // Definir un array que contiene enteros
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Utilizar el método filter del iterador para filtrar el array, seleccionando los números pares
    let even_numbers: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect();

    // Mostrar el resultado filtrado
    println!("Even numbers: {:?}", even_numbers);
}

En el código anterior, primero definimos un array que contiene enterosnumbers, luego utilizamos elfilter()método del iterador para filtrar el array, seleccionando los números pares. En elfilter()cierre (closure) del método, utilizamos la operación de módulo para determinar si un elemento es par. Finalmente utilizamos elcloned()método para clonar el valor de cada número par, y utilizamos elcollect()método para recoger los resultados en un nuevo arrayeven_numbers. Finalmente, se muestra el resultado filtrado.

Al ejecutar el programa, se puede ver que se muestran todos los números pares del array:

Even numbers: [2, 4, 6, 8, 10]

Este ejemplo demuestra el uso delfilter()método del iterador en Rust, así como cómo combinar otros métodos para implementar operaciones de filtrado en arrays.

Métodos de iterador en Rust

A continuación se presentan algunos métodos de iteradores comúnmente utilizados en Rust, junto con sus breves descripciones y ejemplos:

Nombre del método Descripción Ejemplo
next() Devuelve el siguiente elemento del iterador. let mut iter = (1..5).into_iter(); while let Some(val) = iter.next() { println!("{}", val); }
size_hint() Devuelve el límite inferior y superior del número de elementos restantes en el iterador. let iter = (1..10).into_iter(); println!("{:?}", iter.size_hint());
count() Calcula el número de elementos del iterador. let count = (1..10).into_iter().count();
nth() Devuelve el enésimo elemento del iterador. let third = (0..10).into_iter().nth(2);
last() Devuelve el último elemento del iterador. let last = (1..5).into_iter().last();
all() Si todos los elementos del iterador cumplen una condición, devuelvetrue。 let all_positive = (1..=5).into_iter().all(|x| x > 0);
any() Si al menos un elemento del iterador cumple una condición, devuelvetrue。 let any_negative = (1..5).into_iter().any(|x| x < 0);
find() Devuelve el primer elemento del iterador que cumple una condición. let first_even = (1..10).into_iter().find(|x| x % 2 == 0);
find_map() Aplica una función a los elementos del iterador y devuelve el primerSomeresultado devuelto. let first_letter = "hello".chars().find_map(|c| if c.is_alphabetic() { Some(c) } else { None });
map() Aplica una función a cada elemento del iterador. let squares: Vec<i32> = (1..5).into_iter().map(|x| x * x).collect();
filter() Conserva los elementos del iterador que cumplen una condición. let evens: Vec<i32> = (1..10).into_iter().filter(|x| x % 2 == 0).collect();
filter_map() Aplica una función a los elementos del iterador; si la función devuelveSome, se conserva el resultado. let chars: Vec<char> = "hello".chars().filter_map(|c| if c.is_alphabetic() { Some(c.to_ascii_uppercase()) } else { None }).collect();
map_while() Aplica una función a los elementos del iterador hasta que la función devuelvaNone。 let first_three = (1..).into_iter().map_while(|x| if x <= 3 { Some(x) } else { None });
take_while() Toma elementos del iterador que cumplen una condición, hasta que dejan de cumplirla. let first_five = (1..10).into_iter().take_while(|x| x <= 5).collect::<Vec<_>>()
skip_while() Omite elementos del iterador que cumplen una condición, hasta que dejan de cumplirla. let odds: Vec<i32> = (1..10).into_iter().skip_while(|x| x % 2 == 0).collect();
for_each() Ejecuta alguna operación en cada elemento del iterador. let mut counter = 0; (1..5).into_iter().for_each(|x| counter += x);
fold() Pliega los elementos del iterador utilizando un acumulador. let sum: i32 = (1..5).into_iter().fold(0, |acc, x| acc + x);
try_fold() Pliega los elementos del iterador, pudiendo retornar anticipadamente si se encuentra un error. let result: Result = (1..5).into_iter().try_fold(0, |acc, x| if x == 3 { Err("Found the number 3") } else { Ok(acc + x) });
scan() Realiza un plegado con estado sobre los elementos del iterador. let sum: Vec<i32> = (1..5).into_iter().scan(0, |acc, x| { *acc += x; Some(*acc) }).collect();
take() Toma como máximo n elementos del iterador. let first_five = (1..10).into_iter().take(5).collect::<Vec<_>>()
skip() Omite los primeros n elementos del iterador. let after_five = (1..10).into_iter().skip(5).collect::<Vec<_>>()
zip() Empaqueta los elementos de dos iteradores en tuplas. let zipped = (1..3).zip(&['a', 'b', 'c']).collect::<Vec<_>>()
cycle() Repite los elementos del iterador hasta el infinito. let repeated = (1..3).into_iter().cycle().take(7).collect::<Vec<_>>()
chain() Concatena múltiples iteradores. let combined = (1..3).chain(4..6).collect::<Vec<_>>()
rev() Invierte el orden de los elementos del iterador. let reversed = (1..4).into_iter().rev().collect::<Vec<_>>()
enumerate() Añade un índice a cada elemento del iterador. let enumerated = (1..4).into_iter().enumerate().collect::<Vec<_>>()
peeking_take_while() Toma los elementos que cumplen una condición, conservando el estado del iterador, y puede continuar tomando elementos posteriores. let (first, rest) = (1..10).into_iter().peeking_take_while(|&x| x < 5);
step_by() Devuelve los elementos del iterador según un paso especificado. let even_numbers = (0..10).into_iter().step_by(2).collect::<Vec<_>>()
fuse() Crea un iterador adicional que, después de que el iterador se agote, todavía puede invocar alnext()método. let mut iter = (1..5).into_iter().fuse(); while iter.next().is_some() {}
inspect() Ejecuta un cierre (closure) al tomar cada elemento, sin modificar el elemento. let mut counter = 0; (1..5).into_iter().inspect(|x| println!("Inspecting: {}", x)).for_each(|x| println!("Processing: {}", x));
same_items() Compara si dos iteradores producen la misma secuencia de elementos. let equal = (1..5).into_iter().same_items((1..5).into_iter());

Resumen

Los iteradores de Rust son una herramienta potente y flexible que permite procesar secuencias de forma declarativa. El diseño de los iteradores tiene en cuenta la seguridad, el rendimiento y la expresividad, y es una de las características principales del lenguaje Rust. A través de los iteradores, los programadores de Rust pueden escribir código seguro y eficiente.

Otras extensiones