Transformar datos con iteradores y cierres

RustBeginner
Practicar Ahora

Introducción

Un bucle for explícito suele ser la forma más clara de procesar una colección. Rust también proporciona iteradores: valores que producen un elemento cada vez y que se pueden combinar en una canalización legible para procesar datos.

Mantendrá un bucle funcional como referencia, reproducirá su total con iter y sum, escribirá un cierre pequeño, transformará las lecturas con map, rechazará las lecturas no válidas con filter y, por último, combinará todas estas operaciones. Cada modificación se realiza en un proyecto de Cargo preparado, para que pueda concentrarse en los nuevos conceptos de Rust.

Reproducir el total de un bucle con un iterador

En este paso, comparará el bucle for preparado con un iterador que calcula el mismo total.

El proyecto se encuentra en /home/labex/project/reading-pipeline y el programa está en src/main.rs. El vec![12, 18, -1, 24, 30] preparado utiliza la macro vec! como una forma compacta de crear un vector que contiene esos valores; su propósito es equivalente al de crear un vector vacío y añadir cada valor, como hizo en el laboratorio anterior sobre Vec. El bucle existente toma prestado ese vector mediante &readings, recorre cada &i32 y utiliza *reading para acceder al entero al que apunta esa referencia.

Acceda al proyecto y ejecute la versión de referencia:

cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83

El total incluye todas las lecturas, incluida -1. Ahora abra el código fuente:

nano src/main.rs

Sustituya el comentario del paso 1 por:

    let iterator_total: i32 = readings.iter().sum();
    println!("Iterator total: {iterator_total}");

readings.iter() crea un iterador que toma prestado cada elemento del vector. No elimina ni consume el vector. sum() suma repetidamente los elementos producidos; la biblioteca estándar de Rust sabe cómo sumar enteros prestados. La anotación : i32 indica el tipo de resultado que debe construir sum.

Guarde con Ctrl+O, pulse Enter y salga con Ctrl+X. Compruebe el código antes de ejecutarlo:

cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83

Las líneas coincidentes muestran que el bucle y el iterador describen el mismo cálculo. El vector sigue disponible para los pasos posteriores porque iter() solo lo tomó prestado.

Transformar lecturas con un cierre y map

En este paso, definirá un cierre y lo aplicará a cada lectura mediante map.

Un cierre es una función pequeña y sin nombre que se puede almacenar en una variable o pasar directamente a un adaptador de iterador. La forma |parameter: Type| expression coloca los parámetros entre barras verticales y devuelve el valor de la expresión. En este caso, un cierre de calibración añadirá un desplazamiento de dos.

Abra el código fuente:

nano src/main.rs

Sustituya el comentario del paso 2 por:

    let add_offset = |reading: i32| reading + 2;
    let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
    println!("Adjusted all: {adjusted:?}");

map llama a su cierre una vez por cada elemento del iterador y produce los valores devueltos. El flujo de tipos es &i32 de iter() → i32 copiado mediante *reading → i32 ajustado por add_offset → elementos de Vec<i32> mediante collect(). El formateador :? muestra el vector en un formato de depuración legible.

Guarde y salga. Después, compruebe y ejecute el programa:

cargo check
cargo run --quiet

La nueva última línea debe ser:

Adjusted all: [14, 20, 1, 26, 32]

Incluso el valor no válido -1 se transformó en 1; map transforma los elementos, pero no decide qué elementos deben conservarse.

Conservar solo las lecturas válidas con filter

En este paso, utilizará filter para conservar las lecturas no negativas y dejar el vector original sin cambios.

El adaptador filter de un iterador llama a un cierre predicado para cada candidato. Un predicado devuelve true para conservar un elemento o false para rechazarlo. Abra el código fuente:

nano src/main.rs

Sustituya el comentario del paso 3 por:

    let valid: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .collect();
    println!("Valid only: {valid:?}");

Las cadenas de métodos pueden continuar en líneas indentadas después de un punto. iter() toma prestado el vector y copied() convierte cada elemento prestado &i32 en un i32; esto es económico porque los enteros implementan Copy. Después, el predicado de filter recibe una referencia temporal a cada candidato i32, de modo que puede inspeccionarlo sin consumirlo. Por eso la comparación utiliza *reading. El flujo de tipos es &i32 → i32 copiado → i32 conservado → Vec<i32>.

Guarde y salga. Después, ejecute el programa:

cargo run --quiet

La última línea debe ser:

Valid only: [12, 18, 24, 30]

El valor negativo no aparece y los valores conservados mantienen su orden original.

Combinar una canalización completa de procesamiento

En este paso, encadenará el filtrado y la transformación, recopilará las lecturas procesadas y sumará el resultado.

Los adaptadores de iteradores son perezosos: filter y map describen el trabajo, pero los elementos solo pasan por ellos cuando una operación consumidora solicita resultados. collect y sum son operaciones consumidoras. Consumen la canalización del iterador, no el vector de origen tomado en préstamo.

Abra el código fuente:

nano src/main.rs

Sustituya el comentario del paso 4 por:

    let valid_adjusted: Vec<i32> = readings
        .iter()
        .copied()
        .filter(|reading| *reading >= 0)
        .map(add_offset)
        .collect();
    let valid_total: i32 = valid_adjusted.iter().sum();
    println!("Valid adjusted: {valid_adjusted:?}");
    println!("Valid adjusted total: {valid_total}");

La canalización rechaza primero -1. Después, map(add_offset) pasa cada i32 restante al cierre que definió antes. Puede proporcionar directamente el nombre de un cierre cuando sus tipos de parámetros y de retorno son compatibles con el adaptador. El segundo iterador toma prestado el vector recopilado y lo suma.

Guarde y salga. Después, compruebe y ejecute el programa:

cargo check
cargo run --quiet

Las dos últimas líneas deben ser:

Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92

El orden de las operaciones es importante: filtrar antes de transformar evita que la lectura no válida se convierta en un valor positivo aparentemente válido. El vector original readings solo se tomó prestado, por lo que todavía se podía utilizar después.

Resumen

Comparó un bucle explícito con un iterador, escribió y reutilizó un cierre, transformó valores con map, seleccionó valores con filter, recopiló un vector nuevo y calculó un total con sum. También conservó el vector de origen iniciando cada canalización con un iterador prestado.