Traiter une série d'éléments avec des itérateurs

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Processing a Series of Items With Iterators. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons explorer la manière de traiter une série d'éléments à l'aide d'itérateurs, qui sont paresseux et nous permettent d'itérer sur une séquence d'éléments sans avoir à réimplémenter la logique nous-mêmes.

Processing a Series of Items with Iterators

Le patron d'itérateur vous permet d'effectuer une tâche sur une séquence d'éléments tour à tour. Un itérateur est responsable de la logique d'itération sur chaque élément et de la détermination du moment où la séquence est terminée. Lorsque vous utilisez des itérateurs, vous n'avez pas à réimplémenter cette logique vous-même.

En Rust, les itérateurs sont paresseux, ce qui signifie qu'ils n'ont aucun effet jusqu'à ce que vous appeliez des méthodes qui consomment l'itérateur pour l'épuiser. Par exemple, le code de la Liste 13-10 crée un itérateur sur les éléments du vecteur v1 en appelant la méthode iter définie sur Vec<T>. Ce code seul ne fait rien de utile.

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

Liste 13-10: Création d'un itérateur

L'itérateur est stocké dans la variable v1_iter. Une fois que nous avons créé un itérateur, nous pouvons l'utiliser de diverses manières. Dans la Liste 3-5, nous avons itéré sur un tableau en utilisant une boucle for pour exécuter du code sur chacun de ses éléments. Sous le capot, cela a implicitement créé puis consommé un itérateur, mais nous avons passé sous silence jusqu'à présent la manière exacte dont cela fonctionne.

Dans l'exemple de la Liste 13-11, nous séparons la création de l'itérateur de son utilisation dans la boucle for. Lorsque la boucle for est appelée en utilisant l'itérateur dans v1_iter, chaque élément de l'itérateur est utilisé dans une itération de la boucle, ce qui imprime chaque valeur.

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {val}");
}

Liste 13-11: Utilisation d'un itérateur dans une boucle for

Dans les langages qui n'ont pas d'itérateurs fournis par leur bibliothèque standard, vous écririez probablement la même fonctionnalité en commençant une variable à l'index 0, en utilisant cette variable pour indexer dans le vecteur pour obtenir une valeur, et en incrémentant la valeur de la variable dans une boucle jusqu'à ce qu'elle atteigne le nombre total d'éléments dans le vecteur.

Les itérateurs gèrent toute cette logique pour vous, réduisant le code répétitif que vous pourriez potentiellement bousiller. Les itérateurs vous donnent plus de flexibilité pour utiliser la même logique avec de nombreux types différents de séquences, pas seulement les structures de données dans lesquelles vous pouvez indexer, comme les vecteurs. Examnons comment les itérateurs le font.

The Iterator Trait and the next Method

Tous les itérateurs implémentent un trait nommé Iterator qui est défini dans la bibliothèque standard. La définition du trait ressemble à ceci :

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // méthodes avec des implémentations par défaut omises
}

Remarquez que cette définition utilise une nouvelle syntaxe : type Item et Self::Item, qui définissent un type associé avec ce trait. Nous parlerons des types associés en profondeur au Chapitre 19. Pour l'instant, tout ce que vous avez besoin de savoir est que ce code indique que l'implémentation du trait Iterator nécessite également la définition d'un type Item, et ce type Item est utilisé dans le type de retour de la méthode next. En d'autres termes, le type Item sera le type renvoyé par l'itérateur.

Le trait Iterator ne demande aux implémentateurs de définir qu'une seule méthode : la méthode next, qui renvoie un élément de l'itérateur à la fois, emballé dans Some, et, lorsque l'itération est terminée, renvoie None.

Nous pouvons appeler la méthode next directement sur les itérateurs ; la Liste 13-12 montre quelles valeurs sont renvoyées par des appels répétés à next sur l'itérateur créé à partir du vecteur.

Nom du fichier : src/lib.rs

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

Liste 13-12: Appel de la méthode next sur un itérateur

Notez que nous avons dû rendre v1_iter mutable : appeler la méthode next sur un itérateur change l'état interne que l'itérateur utilise pour suivre où il se trouve dans la séquence. En d'autres termes, ce code consomme, ou utilise, l'itérateur. Chaque appel à next consomme un élément de l'itérateur. Nous n'avons pas dû rendre v1_iter mutable lorsque nous avons utilisé une boucle for car la boucle a pris la propriété de v1_iter et l'a rendu mutable en coulisse.

Notez également que les valeurs que nous obtenons des appels à next sont des références immuables aux valeurs dans le vecteur. La méthode iter produit un itérateur sur des références immuables. Si nous voulons créer un itérateur qui prend la propriété de v1 et renvoie des valeurs propriétaires, nous pouvons appeler into_iter au lieu de iter. De même, si nous voulons itérer sur des références mutables, nous pouvons appeler iter_mut au lieu de iter.

Methods That Consume the Iterator

Le trait Iterator a un certain nombre de méthodes différentes avec des implémentations par défaut fournies par la bibliothèque standard ; vous pouvez en découvrir plus sur ces méthodes en consultant la documentation de l'API de la bibliothèque standard pour le trait Iterator. Certaines de ces méthodes appellent la méthode next dans leur définition, voilà pourquoi vous êtes obligé d'implémenter la méthode next lors de l'implémentation du trait Iterator.

Les méthodes qui appellent next sont appelées adapters consommateurs car les appeler utilise l'itérateur. Un exemple est la méthode sum, qui prend la propriété de l'itérateur et itère sur les éléments en appelant répétitivement next, consommant ainsi l'itérateur. Pendant l'itération, elle ajoute chaque élément à un total en cours et renvoie le total une fois l'itération terminée. La Liste 13-13 a un test illustrant l'utilisation de la méthode sum.

Nom du fichier : src/lib.rs

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

Liste 13-13: Appel de la méthode sum pour obtenir le total de tous les éléments de l'itérateur

Nous ne sommes pas autorisés à utiliser v1_iter après l'appel à sum car sum prend la propriété de l'itérateur sur lequel nous l'appelons.

Methods That Produce Other Iterators

Les adapters d'itérateur sont des méthodes définies sur le trait Iterator qui ne consomment pas l'itérateur. Au lieu de cela, elles produisent différents itérateurs en modifiant un aspect de l'itérateur original.

La Liste 13-14 montre un exemple d'appel de la méthode d'adaptateur d'itérateur map, qui prend une closure à appeler sur chaque élément au fur et à mesure que les éléments sont parcourus. La méthode map renvoie un nouvel itérateur qui produit les éléments modifiés. La closure ici crée un nouvel itérateur dans lequel chaque élément du vecteur sera incrémenté de 1.

Nom du fichier : src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

Liste 13-14: Appel de l'adaptateur d'itérateur map pour créer un nouvel itérateur

Cependant, ce code produit un avertissement :

avertissement : `Map` non utilisé qui doit être utilisé
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note : `#[warn(unused_must_use)]` activé par défaut
  = note : les itérateurs sont paresseux et ne font rien tant qu'ils ne sont pas consommés

Le code de la Liste 13-14 ne fait rien ; la closure que nous avons spécifiée n'est jamais appelée. L'avertissement nous rappelle pourquoi : les adapteurs d'itérateur sont paresseux, et nous devons consommer l'itérateur ici.

Pour corriger cet avertissement et consommer l'itérateur, nous utiliserons la méthode collect, que nous avons utilisée avec env::args dans la Liste 12-1. Cette méthode consomme l'itérateur et rassemble les valeurs résultantes dans un type de données de collection.

Dans la Liste 13-15, nous collectons dans un vecteur les résultats de l'itération sur l'itérateur renvoyé par l'appel à map. Ce vecteur finira par contenir chaque élément du vecteur original, incrémenté de 1.

Nom du fichier : src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

Liste 13-15: Appel de la méthode map pour créer un nouvel itérateur, puis appel de la méthode collect pour consommer le nouvel itérateur et créer un vecteur

Comme map prend une closure, nous pouvons spécifier n'importe quelle opération que nous voulons effectuer sur chaque élément. C'est un excellent exemple de la manière dont les closures vous permettent de personnaliser un certain comportement tout en réutilisant le comportement d'itération que le trait Iterator fournit.

Vous pouvez chaîner plusieurs appels à des adapteurs d'itérateur pour effectuer des actions complexes de manière lisible. Mais parce que tous les itérateurs sont paresseux, vous devez appeler l'une des méthodes d'adaptateur consommateur pour obtenir des résultats des appels à des adapteurs d'itérateur.

Using Closures That Capture Their Environment

Plusieurs adapteurs d'itérateur prennent des closures en arguments, et généralement les closures que nous spécifierons en tant qu'arguments pour les adapteurs d'itérateur seront des closures qui capturent leur environnement.

Pour cet exemple, nous utiliserons la méthode filter qui prend une closure. La closure reçoit un élément de l'itérateur et renvoie un bool. Si la closure renvoie true, la valeur sera incluse dans l'itération produite par filter. Si la closure renvoie false, la valeur ne sera pas incluse.

Dans la Liste 13-16, nous utilisons filter avec une closure qui capture la variable shoe_size de son environnement pour itérer sur une collection d'instances de la structure Shoe. Elle ne renverra que les chaussures de la taille spécifiée.

Nom du fichier : src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

Liste 13-16: Utilisation de la méthode filter avec une closure qui capture shoe_size

La fonction shoes_in_size prend la propriété d'un vecteur de chaussures et une taille de chaussure en paramètres. Elle renvoie un vecteur ne contenant que les chaussures de la taille spécifiée.

Dans le corps de shoes_in_size, nous appelons into_iter pour créer un itérateur qui prend la propriété du vecteur. Ensuite, nous appelons filter pour adapter cet itérateur en un nouvel itérateur ne contenant que les éléments pour lesquels la closure renvoie true.

La closure capture le paramètre shoe_size de l'environnement et compare la valeur avec la taille de chaque chaussure, ne conservant que les chaussures de la taille spécifiée. Enfin, l'appel à collect rassemble les valeurs renvoyées par l'itérateur adapté dans un vecteur qui est renvoyé par la fonction.

Le test montre que lorsqu'on appelle shoes_in_size, on obtient seulement les chaussures qui ont la même taille que la valeur que nous avons spécifiée.

Summary

Félicitations ! Vous avez terminé le laboratoire Traitement d'une série d'éléments avec des itérateurs. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.