Transformer des données avec des itérateurs et des closures

RustBeginner
Pratiquer maintenant

Introduction

Une boucle for explicite est souvent la façon la plus claire de traiter une collection. Rust fournit également des itérateurs : des valeurs qui produisent un élément à la fois et que vous pouvez combiner dans un pipeline de traitement des données lisible.

Vous conserverez une boucle fonctionnelle comme référence, reproduirez son total avec iter et sum, écrirez une petite closure, transformerez les lectures avec map, rejetterez les lectures non valides avec filter, puis combinerez toutes ces opérations. Chaque modification s’effectue dans un projet Cargo préparé, afin que vous puissiez vous concentrer sur les nouveaux concepts de Rust.

Reproduire le total d’une boucle avec un itérateur

Dans cette étape, vous comparerez la boucle for préparée avec un itérateur qui calcule le même total.

Le projet se trouve dans /home/labex/project/reading-pipeline et son programme se trouve dans src/main.rs. Le vec![12, 18, -1, 24, 30] préparé utilise la macro vec! pour créer de façon concise un vecteur contenant ces valeurs ; son objectif est équivalent à celui d’un vecteur vide auquel vous ajoutez chaque valeur, comme dans le laboratoire précédent sur les vecteurs. La boucle existante emprunte ce vecteur avec &readings, parcourt chaque &i32 et utilise *reading pour accéder à l’entier référencé.

Accédez au projet et exécutez la version de référence :

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

Le total inclut toutes les lectures, y compris -1. Ouvrez maintenant le code source :

nano src/main.rs

Remplacez le commentaire de l’étape 1 par :

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

readings.iter() crée un itérateur qui emprunte chaque élément du vecteur. Il ne supprime pas le vecteur et ne le consomme pas. sum() additionne successivement les éléments produits ; la bibliothèque standard de Rust sait additionner des entiers empruntés. L’annotation : i32 indique le type du résultat que sum doit construire.

Enregistrez avec Ctrl+O, appuyez sur Entrée, puis quittez avec Ctrl+X. Vérifiez le code avant de l’exécuter :

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

Les deux lignes identiques montrent que la boucle et l’itérateur décrivent le même calcul. Le vecteur reste disponible pour les étapes suivantes, car iter() l’a seulement emprunté.

Transformer les lectures avec une closure et map

Dans cette étape, vous définirez une closure et l’appliquerez à chaque lecture avec map.

Une closure est une petite fonction anonyme qui peut être stockée dans une variable ou transmise directement à un adaptateur d’itérateur. Dans la forme |parameter: Type| expression, les paramètres se trouvent entre des barres verticales et la valeur de l’expression est renvoyée. Ici, une closure d’étalonnage ajoutera un décalage de deux.

Ouvrez le code source :

nano src/main.rs

Remplacez le commentaire de l’étape 2 par :

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

map appelle sa closure une fois pour chaque élément de l’itérateur et produit les valeurs renvoyées. Le cheminement des types est le suivant : &i32 fourni par iter() → i32 copié par *reading → i32 ajusté par add_offset → éléments de Vec<i32> produits par collect(). Le formateur :? affiche le vecteur dans un format de débogage lisible.

Enregistrez et quittez, puis vérifiez et exécutez le programme :

cargo check
cargo run --quiet

La nouvelle dernière ligne doit être :

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

Même la valeur non valide -1 a été transformée en 1 ; map transforme les éléments, mais ne décide pas quels éléments doivent être conservés.

Conserver uniquement les lectures valides avec filter

Dans cette étape, vous utiliserez filter pour conserver les lectures positives ou nulles, sans modifier le vecteur d’origine.

L’adaptateur filter d’un itérateur appelle une closure prédicat pour chaque élément candidat. Un prédicat renvoie true pour conserver un élément et false pour le rejeter. Ouvrez le code source :

nano src/main.rs

Remplacez le commentaire de l’étape 3 par :

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

Les chaînes d’appels de méthodes peuvent continuer sur des lignes indentées après un point. iter() emprunte le vecteur et copied() transforme chaque élément emprunté &i32 en i32 ; cette opération est peu coûteuse, car les entiers implémentent Copy. Le prédicat de filter reçoit ensuite une référence temporaire vers chaque candidat i32, afin de pouvoir l’inspecter sans le consommer ; c’est pourquoi la comparaison utilise *reading. Le cheminement des types est le suivant : &i32 → i32 copié → i32 conservé → Vec<i32>.

Enregistrez et quittez, puis exécutez le programme :

cargo run --quiet

La dernière ligne doit être :

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

La valeur négative est absente, tandis que les valeurs conservées gardent leur ordre d’origine.

Composer un pipeline complet de traitement

Dans cette étape, vous enchaînerez le filtrage et la transformation, collecterez les lectures traitées, puis additionnerez le résultat.

Les adaptateurs d’itérateur sont paresseux : filter et map décrivent le traitement, mais les éléments ne les traversent que lorsqu’une opération consommatrice demande des résultats. collect et sum sont des opérations consommatrices. Elles consomment le pipeline d’itérateurs, et non le vecteur source emprunté.

Ouvrez le code source :

nano src/main.rs

Remplacez le commentaire de l’étape 4 par :

    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}");

Le pipeline rejette d’abord -1, puis map(add_offset) transmet chaque i32 restant à la closure que vous avez définie précédemment. Vous pouvez fournir directement le nom d’une closure lorsque ses types de paramètres et de retour correspondent à ceux de l’adaptateur. Le second itérateur emprunte le vecteur collecté et en calcule la somme.

Enregistrez et quittez, puis vérifiez et exécutez le programme :

cargo check
cargo run --quiet

Les deux dernières lignes doivent être :

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

L’ordre des opérations est important : filtrer avant de transformer empêche la lecture non valide de devenir une valeur positive apparemment valide. Le vecteur d’origine readings a seulement été emprunté et pourrait encore être utilisé ensuite.

Résumé

Vous avez comparé une boucle explicite avec un itérateur, écrit et réutilisé une closure, transformé des valeurs avec map, sélectionné des valeurs avec filter, collecté un nouveau vecteur et calculé un total avec sum. Vous avez également conservé le vecteur source en commençant chaque pipeline par un itérateur emprunté.