Les itérateurs
Les trois méthodes de parcours
| Méthode | Type de l’Item | Effet sur la collection |
|---|---|---|
iter() | &T | Empruntée en lecture, réutilisable ensuite |
iter_mut() | &mut T | Empruntée en écriture, réutilisable ensuite |
into_iter() | T | Consommée, si le receveur est possédé |
La nuance qui échappe le plus souvent : into_iter() est polymorphe sur son receveur.
Il existe en effet trois implémentations distinctes de IntoIterator : une pour Vec<T>, une pour &Vec<T> et une pour &mut Vec<T>. C’est ce qui fait fonctionner for x in &v.
Les trois traits
| Trait | Méthode clé | Signification |
|---|---|---|
Iterator | fn next(&mut self) -> Option<Self::Item> | « je suis un itérateur » |
IntoIterator | fn into_iter(self) -> Self::IntoIter | « je peux devenir un itérateur » |
FromIterator | fn from_iter(iter) -> Self | « je peux être construit depuis un itérateur », ce qu’appelle collect() |
Iterator n’a qu’une seule méthode requise : next(). La soixantaine d’autres (map, filter, fold…) sont des méthodes par défaut définies à partir d’elle. C’est pourquoi écrire un itérateur personnalisé coûte quelques lignes et donne accès à tout l’écosystème.
Note
Il n’existe pas de trait Iter. La méthode iter() est une méthode inhérente à chaque collection, par pure convention. Sur Vec<T>, elle provient même de la slice [T] via Deref : Vec n’a pas de iter() en propre — voir Le mécanisme Deref.
Ce que fait réellement une boucle for
for x in expression { ... } est réécrit par le compilateur en :
Une boucle for appelle donc toujours into_iter(). Ce qui varie, c’est le type de l’expression :
Si v.iter() fonctionne dans un for alors que c’est déjà un itérateur, c’est grâce à une implémentation générale :
Tout Iterator est donc trivialement un IntoIterator.
Tip
Convention d’écriture : for x in &v dans une boucle simple, plus court et plus lisible ; v.iter().filter(...).map(...) dans une chaîne d’adaptateurs, où un Iterator est indispensable.
Le cas des tableaux
Historiquement, [T; N] n’implémentait pas IntoIterator par valeur : tableau.into_iter() passait par auto-référencement vers &[T; N] et produisait des &T. Le défaut a été corrigé, mais avec une compensation pour préserver la compatibilité.
| Édition | tableau.into_iter() |
|---|---|
| 2015 / 2018 | Item = &T, par résolution via auto-référencement |
| 2021 / 2024 | Item = T |
En édition 2021 et suivantes, il n’y a plus de surprise. Du code appelant IntoIterator::into_iter(tableau) en syntaxe de fonction est un contournement de l’époque 2018, qui peut être simplifié.
Note
Une slice [T] n’a pas de taille connue et ne peut donc pas être consommée par valeur : seules &[T] et &mut [T] implémentent IntoIterator. Pour extraire les valeurs d’un Vec sans le détruire, il y a drain().
Tableau par collection
| Collection | iter() | iter_mut() | into_iter() |
|---|---|---|---|
Vec<T> / [T] | &T | &mut T | T |
HashMap<K, V> | (&K, &V) | (&K, &mut V) | (K, V) |
HashSet<T> | &T | — (casserait le hachage) | T |
BTreeMap<K, V> | (&K, &V), trié | (&K, &mut V) | (K, V) |
Option<T> | &T | &mut T | T |
Result<T, E> | &T | &mut T | T |
String | — (chars() ou bytes()) | — | — |
Variantes utiles sur les tables associatives :
Que Option et Result se comportent comme des itérateurs de zéro ou un élément n’est pas une curiosité : c’est ce qui permet à flatten() de fonctionner sur un Iterator<Item = Option<T>>, comme décrit dans Le type Option.
La paresse
Les adaptateurs ne font rien. Ils construisent une structure qui décrit le travail à accomplir, et rien ne s’exécute avant l’intervention d’un consommateur.
Iterator étant annoté #[must_use], le compilateur émet ici un avertissement.
Le corollaire est positif : enchaîner six adaptateurs ne provoque pas six passes sur les données. Tout est fusionné en une seule passe au moment de la consommation, et l’optimiseur produit généralement une boucle équivalente à sa version impérative écrite à la main.
Adaptateurs et consommateurs
Adaptateurs — ils retournent un Iterator
Consommateurs — ils terminent la chaîne
Quelques nuances
filter_mapest préférable àmap(...).filter(...).map(...). La formefilter_map(|x| x.parse().ok())est l’idiome pour « transformer en écartant les échecs ».foldetreducediffèrent par leur point de départ :foldreçoit un accumulateur initial et peut changer de type,reducepart du premier élément et retourne unOption<T>.zips’arrête au plus court, silencieusement. C’est une source de bogues discrets.rev()exige unDoubleEndedIterator: disponible sur les slices,VecetBTreeMap, mais pas surHashMapni derrière un adaptateur qui perd cette propriété.by_ref()emprunte l’itérateur au lieu de le consommer, ce qui permet de le reprendre ensuite :
- Entre
cloned()etcopied(), préférercopied()dès queT: Copy: c’est plus explicite et cela documente l’absence de coût.
collect()
collect() est générique sur son type de retour via FromIterator. La cible doit donc toujours être annotée :
Deux formes méritent une attention particulière :
Cela fonctionne parce que Result<V, E> implémente FromIterator<Result<T, E>> dès lors que V: FromIterator<T>.
Warning
collect() alloue. Il faut enchaîner tous les adaptateurs avant de collecter, et ne jamais insérer un collect() intermédiaire par simple confort de lecture. Si un point de lisibilité est nécessaire, mieux vaut extraire une fonction qui retourne un impl Iterator.
Retourner un itérateur
Trois possibilités, par ordre de préférence :
L’annotation + '_, ou une durée de vie nommée, est obligatoire dès que l’itérateur emprunte self.
Info
En édition 2024, les règles de capture des durées de vie dans un impl Trait ont été assouplies : impl Iterator<Item = &Noeud> capture désormais par défaut les durées de vie en portée, ce qui rend le + '_ souvent superflu.
Écrire son propre itérateur
Une seule méthode à implémenter :
map, filter, take_while ou collect sont alors disponibles sans effort supplémentaire.
Tip
Avant d’en arriver là, il faut vérifier que le module std::iter ne fait pas déjà le travail :
successors couvre la quasi-totalité des remontées de chaîne : parents, liens, pointeurs vers l’élément suivant.
Implémenter IntoIterator pour son propre type
Le pattern canonique consiste à fournir les trois implémentations :
Auxquelles s’ajoutent, par convention, les méthodes inhérentes iter() et iter_mut() qui délèguent aux deux dernières. Le coût est de quelques lignes ; en échange, le type fonctionne dans une boucle for et dans toutes les chaînes d’adaptateurs.
Pièges courants
Mutation pendant l’itération
Le grand classique : une collection ne peut pas être modifiée pendant son parcours.
Les solutions, par ordre de préférence :
Cloner alors que l’on possède la collection
into_iter() est l’outil anti-clone. Dès qu’un .clone() apparaît dans un map, il faut se demander si l’original est encore utile.
Collection déplacée par une boucle for
Symptôme classique de l’oubli du &. Le message du compilateur (borrow of moved value) désigne le bon endroit, mais la cause — la réécriture en into_iter() — n’est pas évidente la première fois.
collect() intermédiaire inutile
count() pour tester la vacuité
count() consomme l’itérateur en entier. Pour savoir s’il reste au moins un élément, next().is_some() ou any(...) suffisent.
chunks et windows ne sont pas des méthodes d’Iterator
Ce sont des méthodes de slice. Dans une chaîne d’itérateurs, il faut soit collecter au préalable, soit passer par la crate itertools.
Performance
Les itérateurs sont l’exemple canonique d’abstraction sans coût : le code généré est en pratique identique à la boucle écrite à la main, parfois meilleur — l’itérateur de slice élimine les vérifications de bornes que v[i] conserve.
Ce qui coûte réellement :
| Cause | Remède |
|---|---|
collect() intermédiaire | Enchaîner davantage |
clone() dans un map | into_iter() |
Box<dyn Iterator> dans une boucle chaude | impl Trait |
| Allocation mal dimensionnée | Vec::with_capacity puis extend |
La méthode size_hint() permet à collect() de pré-réserver l’allocation. Les adaptateurs qui la préservent (map, enumerate, zip) donnent donc de meilleures allocations que ceux qui la perdent (filter, flat_map).
Tip
Comme toujours, il faut mesurer — cargo bench, criterion, perf — avant de dé-idiomatiser du code pour des raisons de performance supposées.
La crate itertools
Elle comble les manques de la bibliothèque standard. Les fonctions les plus utiles :
Aide-mémoire
Ressources
std::iter— la documentation du module explique le modèle mieux que la plupart des tutorielsIterator— parcourir une fois la liste complète des méthodes est un bon investissementitertools— les compléments à la bibliothèque standard- Clippy détecte la plupart des pièges ci-dessus :
needless_collect,map_clone,filter_map_next,into_iter_on_ref,explicit_iter_loop