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();
}