Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Traiter une série d’éléments avec les itérateurs

Le patron de conception iterateur vous permet d’effectuer une tâche sur une séquence d’éléments tour a tour. Un iterateur est responsable de la logique d’iteration sur chaque élément et de la determination de la fin de la séquence. Lorsque vous utilisez des iterateurs, vous n’avez pas a reimplementer cette logique vous-meme.

En Rust, les itérateurs sont paresseux (lazy), ce qui signifie qu’ils n’ont aucun effet tant que vous n’appelez pas de méthodes qui consomment l’itérateur pour l’utiliser. Par exemple, le code de l’encart 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 en lui-même ne fait rien d’utile.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: Creating an iterator

L’iterateur est stocké dans la variable v1_iter. Une fois que nous avons crée un iterateur, nous pouvons l’utiliser de diverses façons. Dans l’encart 3-5, nous avons itere 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ée puis consomme un iterateur, mais nous avons passe sous silence le fonctionnement exact de ce mecanisme jusqu’a maintenant.

Dans l’exemple de l’encart 13-11, nous séparons la création de l’itérateur de l’utilisation de l’itérateur 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 affiche chaque valeur.

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: Using an iterator in a for loop

Dans les langages dont les bibliothèques standard ne fournissent pas d’iterateurs, vous ecririez probablement cette même fonctionnalité en initialisant une variable à l’index 0, en utilisant cette variable pour indexer le vecteur et obtenir une valeur, puis en incrementant la valeur de la variable dans une boucle jusqu’a atteindre le nombre total d’éléments du vecteur.

Les iterateurs gèrent toute cette logique pour vous, reduisant le code repetitif que vous pourriez potentiellement mal écrire. Les iterateurs vous donnent plus de flexibilite pour utiliser la même logique avec de nombreux types différents de séquences, pas seulement des structures de données que vous pouvez indexer, comme les vecteurs. Examinons comment les iterateurs font cela.

Le trait Iterator et la méthode next

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 :

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

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

    // methods with default implementations elided
}
}

Remarquez que cette définition utilise une syntaxe nouvelle : type Item et Self::Item, qui définissent un type associé à ce trait. Nous parlerons des types associés en détail dans le chapitre 20. Pour l’instant, tout ce que vous devez savoir est que ce code dit qu’implémenter le trait Iterator nécessite que vous définissiez également 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 retourné par l’itérateur.

Le trait Iterator n’exige des implémenteurs que la définition d’une seule méthode : la méthode next, qui retourne un élément de l’itérateur à la fois, enveloppé dans Some, et, lorsque l’itération est terminée, retourne None.

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

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[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);
    }
}
Listing 13-12: Calling the next method on an iterator

Notez que nous avons dû rendre v1_iter mutable : appeler la méthode next sur un itérateur modifie l’état interne que l’itérateur utilise pour garder une trace de sa position dans la séquence. En d’autres termes, ce code consomme, ou épuise, l’itérateur. Chaque appel à next consomme un élément de l’itérateur. Nous n’avions pas besoin de rendre v1_iter mutable lorsque nous utilisions une boucle for, car la boucle a pris la possession de v1_iter et l’a rendue mutable en coulisses.

Notez également que les valeurs que nous obtenons des appels à next sont des références immuables vers les valeurs du 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 possession de v1 et retourne des valeurs possédées, 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.

Les méthodes qui consomment l’itérateur

Le trait Iterator possède un certain nombre de méthodes différentes avec des implémentations par défaut fournies par la bibliothèque standard ; vous pouvez découvrir 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, c’est pourquoi vous devez implémenter la méthode next lorsque vous implémentez le trait Iterator.

Les méthodes qui appellent next sont appelées adaptateurs consommateurs car les appeler consomme l’itérateur. Un exemple est la méthode sum, qui prend la possession de l’itérateur et parcourt les éléments en appelant next de manière répétée, consommant ainsi l’itérateur. Au fur et à mesure qu’elle itère, elle ajoute chaque élément à un total cumulatif et retourne le total lorsque l’itération est terminée. L’encart 13-13 contient un test illustrant l’utilisation de la méthode sum.

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[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);
    }
}
Listing 13-13: Calling the sum method to get the total of all items in the iterator

Nous n’avons pas le droit d’utiliser v1_iter après l’appel a sum, car sum prend la possession de l’iterateur sur lequel nous l’appelons.

Les méthodes qui produisent d’autres itérateurs

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

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

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: Calling the iterator adapter map to create a new iterator

Cependant, ce code produit un avertissement :

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Le code de l’encart 13-14 ne fait rien ; la fermeture que nous avons spécifiée n’est jamais appelée. L’avertissement nous rappelle pourquoi : les adaptateurs d’iterateurs sont paresseux, et nous devons consommer l’iterateur 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 l’encart 12-1. Cette méthode consomme l’itérateur et collecte les valeurs résultantes dans un type de données de collection.

Dans l’encart 13-15, nous collectons les résultats de l’iteration sur l’iterateur retourné par l’appel a map dans un vecteur. Ce vecteur finira par contenir chaque élément du vecteur original, incremente de 1.

Filename: src/main.rs
fn main() {
    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]);
}
Listing 13-15: Calling the map method to create a new iterator, and then calling the collect method to consume the new iterator and create a vector

Parce que map prend une fermeture, nous pouvons spécifier n’importe quelle opération que nous voulons effectuer sur chaque élément. C’est un excellent exemple de la façon dont les fermetures vous permettent de personnaliser un comportement tout en réutilisant le comportement d’itération que le trait Iterator fournit.

Vous pouvez enchaîner plusieurs appels à des adaptateurs d’itérateurs 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 à partir des appels aux adaptateurs d’itérateurs.

Les fermetures qui capturent leur environnement

De nombreux adaptateurs d’iterateurs prennent des fermetures en arguments, et généralement les fermetures que nous specifierons comme arguments aux adaptateurs d’iterateurs seront des fermetures qui capturent leur environnement.

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

Dans l’encart 13-16, nous utilisons filter avec une fermeture qui capture la variable shoe_size de son environnement pour iterer sur une collection d’instances de la struct Shoe. Elle ne retournera que les chaussures de la taille spécifiée.

Filename: 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")
                },
            ]
        );
    }
}
Listing 13-16: Using the filter method with a closure that captures shoe_size

La fonction shoes_in_size prend la possession d’un vecteur de chaussures et une taille de chaussure en paramètres. Elle retourne un vecteur contenant uniquement les chaussures de la taille spécifiée.

Dans le corps de shoes_in_size, nous appelons into_iter pour créer un iterateur qui prend la possession du vecteur. Ensuite, nous appelons filter pour adapter cet iterateur en un nouvel iterateur qui ne contient que les éléments pour lesquels la fermeture retourné true.

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

Le test montre que lorsque nous appelons shoes_in_size, nous ne recuperons que les chaussures qui ont la même taille que la valeur que nous avons spécifiée.