Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

13. Les itérateurs

Un problème couramment rencontré par les débutants en Rust est l'implémentation du trait Iterator. Nous allons donc tenter de remédier à cela en expliquant comme il fonctionne.

Jusqu'ici, nous savons qu'il existe deux types d'Iterators :

  • Les itérateurs sur/liés à un type.
  • Les générateurs.

Les itérateurs sur/liés à un type

Ce type va itérer sur un ensemble de données. Bien que cette approche reste la plus complexe des deux (à cause des durées de vie notamment), sa mise en place n'a rien d'insurmontable.

Imaginons que vous ayez besoin de wrapper un Vec tout en ayant la capacité d'itérer sur le type fraîchement créé pour l'occasion.

Définissons la structure proprement dite :

Runstruct NewType<T>(Vec<T>);

Nous allons, maintenant, avoir besoin d'implémenter le trait Iterator. Le principal problème est que vous ne pouvez pas stocker un paramètre dans la structure NewType qui pourrait vous permettre de suivre la progression de la lecture à l'intérieur de votre vecteur et... c'est ici que la plupart des gens sont perdus. La solution est en réalité plutôt simple :

Run// On crée une nouvelle structure qui contiendra une référence de votre ensemble
// de données.
struct IterNewType<'a, T: 'a> {
    inner: &'a NewType<T>,
    // Ici, nous utiliserons `pos` pour suivre la progression de notre
    // itération.
    pos: usize,
}

// Il ne nous reste plus alors qu'à implémenter le trait `Iterator` pour
// `IterNewType`.
impl<'a, T> Iterator for IterNewType<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.pos >= self.inner.0.len() {
            // Il n'y a plus de données à lire, on stoppe l'itération.
            None
        } else {
            // On incrémente la position de notre itérateur.
            self.pos += 1;
            // On renvoie la valeur courante pointée par notre itérateur.
            self.inner.0.get(self.pos - 1)
        }
    }
}

Simple, non ? Il nous reste plus qu'à ajouter la méthode iter à notre structure NewType :

Runimpl<T> NewType<T> {
    fn iter<'a>(&'a self) -> IterNewType<'a, T> {
        IterNewType {
            inner: self,
            pos: 0,
        }
    }
}

Fini !

Voici un petit exemple d'utilisation de notre structure :

Runfor x in NewType(vec![1, 3, 5, 8]).iter() {
    println!("=> {}", x);
}

Résultat :

=> 1
=> 3
=> 5
=> 8

Les générateurs

Un générateur est une manière plutôt intéressante (et simple) d'utiliser les Iterators en Rust. Un exemple sera certainement plus parlant dans ce cas précis :

Run// Notre structure itère (on peut aussi dire "génère") uniquement sur les
// nombres impairs.
struct Impair {
    current: usize,
}

impl Impair {
    fn new() -> Impair {
        Impair {
            // La première valeur impaire positive est 1, donc commençons à 1.
            current: 1,
        }
    }
}

impl Iterator for Impair {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        // Déplaçons-nous à la valeur impaire suivante.
        self.current += 2;
        // On renvoie la valeur impaire courante.
        Some(self.current - 2)
    }
}

fn main() {
    // Pour éviter de boucler indéfiniment avec notre itérateur `Impair`, nous
    // avons limité la boucle à 3 valeurs.
    for x in Impair::new().take(3) {
        println!("=> {}", x);
    }
}

Résultat :

=> 1
=> 3
=> 5

Comme vous pouvez le constater, Impair génère ses propres valeurs, contrairement à l'exemple
précédent qui était basé sur celles d'un vecteur. Sa conception rend la génération infinie, mais
il est tout à fait possible d'établir une limite (aussi bien interne à la structure que dans son
utilisation). À vous de voir selon vos besoins !

Par-exemple, si on créait un itérateur sur des nombres premiers, il ne pourrait continuer
que jusqu'au dernier nombre premier connu (ou alors vous possédez un data-center personnel).

Conclusion

Les itérateurs peuvent se montrer puissants et restent relativement simples à implémenter en Rust, mais
les débutants ont tendance à directement gérer la ressource et itérer dessus, ce qui complique
généralement la recherche de solutions potentiellement plus adaptées.

Il est toujours question de penser "Rust" ou non !

Article original

Ce chapitre a été écrit à partir de cet article de blog. N'hésitez pas à y faire un tour !