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 iterateurs sont paresseux (lazy), ce qui signifie qu’ils n’ont aucun effet tant que vous n’appelez pas de methodes qui consomment l’iterateur pour l’utiliser. Par exemple, le code de l’encart 13-10 crée un iterateur sur les éléments du vecteur v1 en appelant la methode iter définie sur Vec<T>. Ce code en lui-meme 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 separons la creation de l’iterateur de l’utilisation de l’iterateur dans la boucle for. Lorsque la boucle for est appelée en utilisant l’iterateur dans v1_iter, chaque élément de l’iterateur est utilise dans une iteration 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 methode next

Tous les iterateurs implementent un trait nomme Iterator qui est défini dans la bibliothèque standard. La définition du trait ressemble a ceci :

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

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

    // methods with default implémentations elided
}
}

Remarquez que cette définition utilise une syntaxe nouvelle : type Item et Self::Item, qui définissent un type associe à ce trait. Nous parlerons des types associes 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 definissiez également un type Item, et ce type Item est utilise dans le type de retour de la methode next. En d’autres termes, le type Item sera le type retourné par l’iterateur.

Le trait Iterator n’exige des implementeurs que la définition d’une seule methode : la methode next, qui retourné un élément de l’iterateur à la fois, enveloppe dans Some, et, lorsque l’iteration est terminée, retourné None.

Nous pouvons appeler la methode next directement sur les iterateurs ; l’encart 13-12 montre quelles valeurs sont retournées par des appels repetes a next sur l’iterateur crée à 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 du rendre v1_iter mutable : appeler la methode next sur un iterateur modifié l’état interne que l’iterateur utilise pour garder une trace de sa position dans la séquence. En d’autres termes, ce code consomme, ou epuise, l’iterateur. Chaque appel a next consomme un élément de l’iterateur. 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 a next sont des références immuables vers les valeurs du vecteur. La methode iter produit un iterateur sur des références immuables. Si nous voulons créer un iterateur qui prend la possession de v1 et retourné des valeurs possedees, nous pouvons appeler into_iter au lieu de iter. De même, si nous voulons iterer sur des références mutables, nous pouvons appeler iter_mut au lieu de iter.

Les methodes qui consomment l’iterateur

Le trait Iterator possède un certain nombre de methodes différentes avec des implémentations par défaut fournies par la bibliothèque standard ; vous pouvez découvrir ces methodes en consultant la documentation de l’API de la bibliothèque standard pour le trait Iterator. Certaines de ces methodes appellent la methode next dans leur définition, c’est pourquoi vous devez implémenter la methode next lorsque vous implementez le trait Iterator.

Les methodes qui appellent next sont appelées adaptateurs consommateurs car les appeler consomme l’iterateur. Un exemple est la methode sum, qui prend la possession de l’iterateur et parcourt les éléments en appelant next de maniere repetee, consommant ainsi l’iterateur. Au fur et a mesure qu’elle itere, elle ajouté chaque élément à un total cumulatif et retourné le total lorsque l’iteration est terminée. L’encart 13-13 contient un test illustrant l’utilisation de la methode 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 methodes qui produisent d’autres iterateurs

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

L’encart 13-14 montre un exemple d’appel de la methode d’adaptateur d’iterateur map, qui prend une fermeture a appeler sur chaque élément au fur et a mesure que les éléments sont parcourus. La methode map retourné un nouvel iterateur qui produit les éléments modifiés. La fermeture ici crée un nouvel iterateur dans lequel chaque élément du vecteur sera incremente 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 : console {{#include ../listings/ch13-functional-features/listing-13-14/output.txt}}

$ 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)]` 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’iterateur, nous utiliserons la methode collect, que nous avons utilisee avec env::args dans l’encart 12-1. Cette methode consomme l’iterateur et collecte les valeurs resultantes 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 reutilisant le comportement d’iteration que le trait Iterator fournit.

Vous pouvez enchainer plusieurs appels à des adaptateurs d’iterateurs pour effectuer des actions complexes de maniere lisible. Mais parce que tous les iterateurs sont paresseux, vous devez appeler l’une des methodes d’adaptateur consommateur pour obtenir des résultats à partir des appels aux adaptateurs d’iterateurs.

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 methode filter qui prend une fermeture. La fermeture recoit un élément de l’iterateur et retourné un bool. Si la fermeture retourné true, la valeur sera incluse dans l’iteration produite par filter. Si la fermeture retourné 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 retourné 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 a collect rassemble les valeurs retournées par l’iterateur adapte 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.