Aller au contenu

Les itérateurs en Rust

Un itérateur, c’est tout ce qui peut produire une suite de valeurs, une à la fois. Vous en utilisez depuis la page For sans forcément les nommer : chaque boucle for sur une collection repose en coulisses sur un itérateur.

fn main() {
let numbers = vec![1, 2, 3];
let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
}

Sortie :

Some(1)
Some(2)
Some(3)
None
▶ Essayez par vous-même

.next() renvoie une Option : Some(valeur) pour chaque élément, puis None quand il n’y en a plus. Une boucle for ne fait rien d’autre qu’appeler .next() encore et encore, jusqu’à obtenir None.

.map() prend une closure (fonction anonyme) et l’applique à chaque élément, ce qui produit un nouvel itérateur :

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Sortie :

[2, 4, 6, 8, 10]
▶ Essayez par vous-même

C’est .collect() qui retransforme l’itérateur en une vraie collection (ici, un Vec<i32>). Sans lui, vous auriez simplement un itérateur qui attend, sans que rien ne soit calculé.

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|n| **n % 2 == 0).collect();
println!("{:?}", evens);
}

Sortie :

[2, 4, 6]
▶ Essayez par vous-même

Le double **n a l’air étrange. .filter() donne à la closure une référence vers une référence (&&i32), il faut donc la déréférencer deux fois pour la comparer à un nombre ordinaire. C’est une petite bizarrerie courante ; ne vous inquiétez pas encore de l’explication complète.

C’est là que l’enchaînement des méthodes d’itérateurs devient vraiment intéressant : vous pouvez combiner plusieurs étapes en un seul « pipeline » facile à lire :

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|n| **n % 2 == 0) // garder les nombres pairs
.map(|n| n * 10) // multiplier chacun par 10
.collect();
println!("{:?}", result);
}

Sortie :

[20, 40, 60]
▶ Essayez par vous-même

Comparez avec la même logique écrite à la main : une boucle for et un Vec vide dans lequel vous faites des push. Ce n’est pas faux, mais la version enchaînée se lit presque comme une phrase : « filtrer, puis multiplier, puis collecter ». C’est exactement la suite d’étapes que vous décrivez.

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
let count = numbers.iter().count();
let has_even = numbers.iter().any(|n| n % 2 == 0);
println!("somme : {total}, nombre : {count}, au moins un pair : {has_even}");
}

Sortie :

somme : 15, nombre : 5, au moins un pair : true
▶ Essayez par vous-même

.sum() additionne tout, .count() vous dit combien il y a d’éléments, et .any() répond à la question « est-ce qu’au moins un élément correspond ? ». Ce ne sont que quelques-unes des méthodes que possède chaque itérateur. Il y en a beaucoup d’autres, comme enumerate, zip, take ou rev. À la page Itérateurs personnalisés, vous construirez votre propre itérateur et vous verrez qu’il obtient toutes ces méthodes gratuitement.

Prenez les nombres de 1 à 10, gardez seulement les impairs, élevez chacun au carré, puis additionnez le tout. Faites-le en une seule chaîne d’itérateurs, sans aucune variable mut. Indice : enchaînez .filter(), .map() et .sum(), et donnez un type au résultat, par exemple let total: i32.

Voir la solution
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total: i32 = numbers
.iter()
.filter(|n| **n % 2 == 1) // garde 1, 3, 5, 7, 9
.map(|n| n * n) // 1, 9, 25, 49, 81
.sum();
println!("somme des carrés impairs : {total}");
}

Sortie :

somme des carrés impairs : 165
▶ Essayez par vous-même

Rust a besoin du : i32 sur total, car .sum() peut produire plusieurs types de nombres différents, et il ne peut pas deviner celui que vous voulez.

Partez de vec!["the", "quick", "brown", "fox", "jumps"]. Construisez un nouveau Vec<String> qui ne contient que les mots de plus de 3 lettres, en majuscules, et affichez-le avec {:?}. Indice : .filter(|w| w.len() > 3) garde les mots longs, et .to_uppercase() transforme un &str en String en majuscules.

Les mots restent en anglais ici, car .len() compte des octets : une lettre accentuée en prend 2, ce qui fausserait le résultat.

Voir la solution
fn main() {
let words = vec!["the", "quick", "brown", "fox", "jumps"];
let loud: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", loud);
}

Sortie :

["QUICK", "BROWN", "JUMPS"]
▶ Essayez par vous-même

words lui-même ne change pas. .iter() ne fait que l’emprunter, vous pouvez donc toujours utiliser words après la chaîne.

  • Un itérateur produit des valeurs une par une grâce à .next(), qui renvoie une Option.
  • En coulisses, les boucles for reposent sur des itérateurs.
  • .map() transforme chaque élément ; .filter() ne garde que ceux qui remplissent une condition ; .collect() retransforme le résultat en une vraie collection.
  • Enchaîner des méthodes d’itérateurs est généralement plus clair qu’une boucle écrite à la main avec un accumulateur mutable.

Petit quiz

1. Que renvoie .next() sur un itérateur ?

2. Que fait .collect() à la fin d'une chaîne comme .iter().map(...).collect() ?

3. Dans .filter(|n| **n % 2 == 0), pourquoi n est-il déréférencé deux fois ?

4. Qu'est-ce qui fait tourner une boucle for quand elle parcourt une collection ?

5. Que garde .filter() dans l'itérateur obtenu ?

Score : 0 / 5