Structures de contrôle#
En Rust, les structures de controle ne se contentent pas de diriger le flot d’exécution : elles participent au calcul. Cette propriété, héritée des langages fonctionnels, constitue l’un des traits les plus distinctifs du langage.
Expressions et instructions#
Définition 24 (Expression)
Une expression est un fragment de code qui s’évalue pour produire une valeur. En Rust, presque toute construction syntaxique est une expression : les littéraux, les opérations arithmétiques, les appels de fonctions, les blocs, les conditionnelles et certaines boucles.
Définition 25 (Instruction)
Une instruction (statement) est un fragment de code qui effectue une action sans produire de valeur exploitable. En Rust, les deux formes principales d’instructions sont les déclarations de liaison (let) et les expressions terminées par un point-virgule.
Remarque 14
En Rust, la frontière entre expression et instruction est ténue. Un bloc { ... } est une expression, une conditionnelle if/else est une expression, une boucle loop peut être une expression. Ce choix de conception permet d’écrire du code concis où le résultat d’une structure de controle est directement assignable à une variable, sans recourir à une variable mutable intermédiaire.
Le point-virgule joue un role central : il transforme une expression en instruction en écartant sa valeur de retour. Omettre le point-virgule sur la dernière ligne d’un bloc en fait la valeur de retour de ce bloc.
let x = {
let a = 3;
let b = 4;
a + b
};
println!("x = {}", x);
x = 7
Dans cet exemple, le bloc { let a = 3; let b = 4; a + b } est une expression dont la valeur est 7. Si l’on ajoutait un point-virgule après a + b, le bloc retournerait () (le type unité).
Conditionnelles#
if / else comme expression#
En Rust, if/else n’est pas seulement une structure de controle : c’est une expression qui produit une valeur. Les deux branches doivent alors retourner des valeurs de meme type.
Exemple 18 (if comme expression)
Voir le code ci-dessous.
let temperature = 38;
let etat = if temperature > 37 {
"fièvre"
} else if temperature < 36 {
"hypothermie"
} else {
"normal"
};
println!("État : {}", etat);
État : fièvre
On peut bien entendu utiliser if de manière classique, comme simple instruction de branchement, sans exploiter sa valeur de retour.
Introduction à if let#
La construction if let permet de tester si une valeur correspond à un motif donné, tout en extrayant les données qu’il contient. Elle sera approfondie au chapitre 10 consacré au filtrage par motifs.
let valeur: Option<i32> = Some(42);
if let Some(n) = valeur {
println!("La valeur est {}", n);
} else {
println!("Aucune valeur");
}
La valeur est 42
()
Boucles#
Rust propose trois constructions de boucle, chacune adaptée à un usage précis.
loop : boucle infinie#
La boucle loop s’exécute indéfiniment jusqu’à ce qu’un break l’interrompe. Sa particularité est que break peut porter une valeur, faisant de loop une expression.
let mut compteur = 0;
let resultat = loop {
compteur += 1;
if compteur == 10 {
break compteur * 2;
}
};
println!("Résultat : {}", resultat);
Résultat : 20
while : boucle conditionnelle#
La boucle while s’exécute tant que sa condition est vraie. Contrairement à loop, elle ne peut pas retourner de valeur via break (elle produit toujours ()).
let mut n = 1;
while n < 100 {
n *= 2;
}
println!("Premier dépassement de 100 : {}", n);
Premier dépassement de 100 : 128
for : itération#
La boucle for itère sur tout objet implémentant le trait Iterator. Elle est idiomatique pour parcourir des plages (ranges) et des collections.
// Itération sur une plage (Range)
for i in 0..5 {
print!("{} ", i);
}
println!();
// Plage inclusive
for i in 1..=3 {
print!("{} ", i);
}
println!();
0 1 2 3 4
1 2 3
// Itération sur une collection
let langages = ["Rust", "C", "OCaml", "Haskell"];
for langage in &langages {
println!("{}", langage);
}
Rust
C
OCaml
Haskell
()
On notera l’utilisation de &langages pour emprunter le tableau plutot que de le consommer. Les notions de possession et d’emprunt seront traitées aux chapitres 5 et 6.
Labels de boucles#
Lorsque des boucles sont imbriquées, il est parfois nécessaire de cibler un break ou un continue sur une boucle extérieure. Rust permet d’étiqueter les boucles avec un label précédé d’une apostrophe.
Exemple 19 (Boucles avec labels)
Voir le code ci-dessous.
let mut total = 0;
'externe: for i in 1..=5 {
for j in 1..=5 {
if i + j > 6 {
continue 'externe;
}
total += 1;
}
}
println!("Nombre de paires (i, j) avec i + j <= 6 : {}", total);
Nombre de paires (i, j) avec i + j <= 6 : 15
Un break étiqueté peut également porter une valeur dans le cas d’une boucle loop.
let valeur = 'recherche: loop {
for x in 0..10 {
if x * x > 50 {
break 'recherche x;
}
}
};
println!("Premier entier dont le carré dépasse 50 : {}", valeur);
Premier entier dont le carré dépasse 50 : 8
Fonctions#
Déclaration#
Une fonction se déclare avec le mot-clé fn, suivi de son nom, de ses paramètres typés et, le cas échéant, de son type de retour.
fn aire_rectangle(largeur: f64, hauteur: f64) -> f64 {
largeur * hauteur
}
let a = aire_rectangle(3.0, 4.5);
println!("Aire : {}", a);
Aire : 13.5
Retour implicite et retour explicite#
Le corps d’une fonction est un bloc. La dernière expression de ce bloc, sans point-virgule, constitue la valeur de retour implicite. Le mot-clé return permet un retour anticipé explicite.
Exemple 20 (Retour implicite et explicite)
Voir le code ci-dessous.
fn valeur_absolue(x: i32) -> i32 {
if x < 0 {
return -x; // retour anticipé explicite
}
x // retour implicite (dernière expression)
}
println!("|−7| = {}", valeur_absolue(-7));
println!("|3| = {}", valeur_absolue(3));
|−7| = 7
|3| = 3
Le style idiomatique en Rust favorise le retour implicite. Le return explicite est réservé aux sorties anticipées, lorsque le flot d’exécution doit quitter la fonction avant sa fin naturelle.
Fonctions comme expressions#
Les appels de fonctions sont eux-memes des expressions. On peut donc les composer librement dans d’autres expressions.
fn carre(x: i32) -> i32 {
x * x
}
fn somme_carres(a: i32, b: i32) -> i32 {
carre(a) + carre(b)
}
let r = somme_carres(3, 4);
println!("3² + 4² = {}", r);
3² + 4² = 25
Les fonctions qui ne retournent pas de valeur significative retournent implicitement le type unité (). Déclarer le type de retour est alors facultatif.
fn saluer(nom: &str) {
println!("Bonjour, {} !", nom);
}
saluer("Ferris");
Bonjour, Ferris !
Le type never !#
Définition 26 (Divergence)
Une expression est dite divergente si elle ne produit jamais de valeur : elle ne termine pas ou interrompt le programme. En Rust, le type de ces expressions est noté ! (never type). Ce type est inhabité : aucune valeur ne peut le posséder.
Le type ! intervient dans plusieurs situations courantes :
la macro
panic!, qui interrompt le programme ;une boucle
loopsansbreak, qui ne termine jamais ;la fonction
std::process::exit, qui arrête le processus.
Le type ! peut se convertir implicitement en tout autre type. C’est pourquoi une branche contenant panic! peut coexister avec des branches retournant un entier dans un if/else ou un match.
fn diviser(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Division par zéro !");
}
a / b
}
println!("10 / 3 = {:.4}", diviser(10.0, 3.0));
10 / 3 = 3.3333
Dans cet exemple, la branche panic! est de type !, ce qui est compatible avec le type f64 de la branche principale.
Introduction à match#
L’expression match compare une valeur à une série de motifs et exécute le bras correspondant au premier motif reconnu. Chaque bras est une expression ; match dans son ensemble est donc une expression.
let code = 404;
let message = match code {
200 => "OK",
301 => "Redirection permanente",
404 => "Non trouvé",
500 => "Erreur interne",
_ => "Code inconnu",
};
println!("{} : {}", code, message);
404 : Non trouvé
Le motif _ joue le role de cas par défaut : il capture toute valeur non couverte par les bras précédents. Le compilateur exige que le match soit exhaustif, c’est-à-dire que tous les cas possibles soient couverts.
Le filtrage par motifs est l’un des mécanismes les plus puissants de Rust. Il sera traité en profondeur au chapitre 10, une fois les énumérations et les structures introduites.