Boucles
Formes
Venant de C++, Rust a un nombre inhabituel de types de boucles.
Il y a d’abord la boucle infinie…
#![allow(unused)] fn main() { // N'exécutez pas ce code ;) loop { println!("Salut, je m'appelle Horace"); } }
…la boucle while
avec une condition, familière quand on vient de C++…
#![allow(unused)] fn main() { let condition = false; while condition { println!("La condition est encore vraie"); } println!("La condition n'est plus vraie"); }
…sa variante while let
qui fait du pattern matching comme if let
…
#![allow(unused)] fn main() { enum PeutEtre { Oui(u32), Non } fn calcul() -> PeutEtre { PeutEtre::Non } while let PeutEtre::Oui(x) = calcul() { println!("Le calcul a encore retourné Oui avec le contenu {x}"); } }
…et une boucle qui accepte des objets itérables en paramètre, qui peuvent être
des itérateurs (implémentant le trait Iterator
) ou des objets itérables
(implémentant le trait IntoIterator
qui permet de créer un itérateur pour
itérer dessus).
#![allow(unused)] fn main() { println!("Boucle basée sur un itérateur (trait Iterator)"); for c in "Ge\u{0301}nial".chars() { println!("- {c}"); } println!(); println!("Boucle basée sur un objet itérable (trait IntoIterator)"); for valeur in [1.2, 3.4, 5.6] { println!("- {valeur}"); } }
Nous reviendrons sur cette boucle un peu plus tard, lorsque nous aurons traité la notion d’itérateur. Mais pour l’heure, retenez qu’il y a une différence terminologique entre C++ et Rust : les itérateurs de Rust correspondent aux ranges de C++20, pas aux itérateurs historiques de C++.
Itérer sur des entiers
Une chose qu’on veut généralement faire quand on est habitué au C++, c’est itérer sur des entiers. C’est possible via la syntaxe des intervalles (ranges) :
#![allow(unused)] fn main() { // Intervalle fermé à gauche, ouvert à droite (comme le range() de Python) for i in 0..4 { println!("{i}"); } println!(); // Intervalle fermé à gauche et à droite for j in 2..=4 { println!("{j}"); } }
Mais ce type d’itération est moins souvent utilisé en Rust, car on lui préfère habituellement l’itération sur les conteneurs. Nous reviendrons sur cette question dans le chapitre sur les itérateurs.
Contrôle
Au sein d’une boucle, on peut utiliser les habituelles instructions break
et
continue
pour affecter le déroulement de la boucle :
#![allow(unused)] fn main() { let condition = false; loop { println!("Bonjour"); if condition { continue; } else { break; } } println!("Au revoir"); }
Et comme Rust est un langage orienté expressions, les boucles infinies peuvent
retourner une expression comme les autres structures du langage, grâce à la
forme de break
qui prend une valeur en paramètre (le break;
sans argument
étant équivalent à break ();
).
#![allow(unused)] fn main() { let resultat = loop { println!("Itération de boucle"); break 42; }; println!("La réponse est {resultat}"); }
Un problème bien connu de break
et continue
est qu’ils ne fonctionnent pas
toujours comme on veut lorsqu’on a plusieurs boucles imbriquées. Rust résout ce
problème en permettant de nommer les boucles pour clarifier de quelle boucle on
parle :
#![allow(unused)] fn main() { 'externe: loop { loop { break 'externe; } } }
Et comme break
est un outil bien pratique pour la gestion de conditions
exceptionnelles, Rust permet de l’utiliser en-dehors des boucles via les blocs
nommés, qui se comportent comme une boucle d’une seule itération du point de
vue de break
.
#![allow(unused)] fn main() { let traitement1_ok = || true; let traitement2_ok = || false; let traitement1 = || (); let traitement2 = || (); let nettoyage = || (); 'bloc: { if !traitement1_ok() { break 'bloc; } traitement1(); if !traitement2_ok() { break 'bloc; } traitement2(); } nettoyage(); }