Les structures de contrôle
La capacité d’exécuter du code selon qu’une condition est true et la capacité d’exécuter du code de manière répétée tant qu’une condition est true sont des éléments fondamentaux dans la plupart des langages de programmation. Les constructions les plus courantes qui vous permettent de contrôler le flux d’exécution du code Rust sont les expressions if et les boucles.
Les expressions if
Une expression if vous permet de créer des embranchements dans votre code en fonction de conditions. Vous fournissez une condition puis déclarez : “Si cette condition est remplie, exécute ce bloc de code. Si la condition n’est pas remplie, n’exécute pas ce bloc de code.”
Créez un nouveau projet appelé branches dans votre répertoire projects pour explorer l’expression if. Dans le fichier src/main.rs, saisissez ce qui suit :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Toutes les expressions if commencent par le mot-clé if, suivi d’une condition. Dans ce cas, la condition vérifie si la variable number à une valeur inférieure à 5. Nous plaçons le bloc de code à exécuter si la condition est true immédiatement après la condition, entre accolades. Les blocs de code associés aux conditions dans les expressions if sont parfois appelés des branches (arms), tout comme les branches dans les expressions match que nous avons abordées dans la section [“Comparing the Guess to the Secret Number”][comparing-the-guess-to-the-secret-number] du chapitre 2.
Optionnellement, nous pouvons également inclure une expression else, ce que nous avons choisi de faire ici, pour donner au programme un bloc de code alternatif à exécuter si la condition s’évalue à false. Si vous ne fournissez pas d’expression else et que la condition est false, le programme passera simplement le bloc if et continuera avec le code suivant.
Essayez d’exécuter ce code ; vous devriez voir la sortie suivante : console {{#include ../listings/ch03-common-programming-concepts/no-listing-26-if-true/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
Essayons de changer la valeur de number pour une valeur qui rend la condition false afin de voir ce qui se passe : rust,ignore {{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-27-if-false/src/main.rs:here}}
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
Exécutez le programme à nouveau et observez la sortie : console {{#include ../listings/ch03-common-programming-concepts/no-listing-27-if-false/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
Il est également important de noter que la condition dans ce code doit être un bool. Si la condition n’est pas un bool, nous obtiendrons une erreur. Par exemple, essayez d’exécuter le code suivant :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
La condition if s’évalue à une valeur de 3 cette fois, et Rust génère une erreur : console {{#include ../listings/ch03-common-programming-concepts/no-listing-28-if-condition-must-be-bool/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
L’erreur indique que Rust attendait un bool mais a reçu un entier. Contrairement à des langages comme Ruby et JavaScript, Rust ne tentera pas automatiquement de convertir des types non-booléens en booléen. Vous devez être explicite et toujours fournir un booléen comme condition au if. Si nous voulons que le bloc de code if ne s’exécute que lorsqu’un nombre n’est pas égal à 0, par exemple, nous pouvons modifier l’expression if comme suit :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
L’exécution de ce code affichera number was something other than zero.
Gérer plusieurs conditions avec else if
Vous pouvez utiliser plusieurs conditions en combinant if et else dans une expression else if. Par exemple :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
Ce programme a quatre chemins possibles qu’il peut emprunter. Après l’avoir exécuté, vous devriez voir la sortie suivante : console {{#include ../listings/ch03-common-programming-concepts/no-listing-30-else-if/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
Lorsque ce programme s’exécute, il vérifie chaque expression if tour à tour et exécute le premier corps pour lequel la condition s’évalue à true. Notez que même si 6 est divisible par 2, nous ne voyons pas la sortie number is divisible by 2, ni le texte number is not divisible by 4, 3, or 2 du bloc else. C’est parce que Rust n’exécute que le bloc de la première condition true, et une fois qu’il en a trouvé une, il ne vérifie même pas les suivantes.
Utiliser trop d’expressions else if peut encombrer votre code, donc si vous en avez plus d’une, vous voudrez peut-être restructurer votre code. Le chapitre 6 décrit une construction de branchement puissante de Rust appelée match pour ces cas-là.
Utiliser if dans une instruction let
Comme if est une expression, nous pouvons l’utiliser du côté droit d’une instruction let pour assigner le résultat à une variable, comme dans le listing 3-2.
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
if expression to a variableLa variable number sera liée à une valeur en fonction du résultat de l’expression if. Exécutez ce code pour voir ce qui se passe : console {{#include ../listings/ch03-common-programming-concepts/listing-03-02/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
Rappelez-vous que les blocs de code s’évaluent à la dernière expression qu’ils contiennent, et que les nombres seuls sont aussi des expressions. Dans ce cas, la valeur de l’expression if entière dépend du bloc de code qui s’exécute. Cela signifie que les valeurs qui peuvent potentiellement être les résultats de chaque branche du if doivent être du même type ; dans le listing 3-2, les résultats de la branche if et de la branche else étaient tous deux des entiers i32. Si les types ne correspondent pas, comme dans l’exemple suivant, nous obtiendrons une erreur :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
Lorsque nous essayons de compiler ce code, nous obtenons une erreur. Les branches if et else ont des types de valeurs incompatibles, et Rust indique exactement où trouver le problème dans le programme : console {{#include ../listings/ch03-common-programming-concepts/no-listing-31-arms-must-return-same-type/output.txt}}
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
L’expression dans le bloc if s’évalue en un entier, et l’expression dans le bloc else s’évalue en une chaîne de caractères. Cela ne fonctionnera pas, car les variables doivent avoir un seul type, et Rust doit savoir de manière définitive au moment de la compilation quel est le type de la variable number. Connaître le type de number permet au compilateur de vérifier que le type est valide partout où nous utilisons number. Rust ne pourrait pas faire cela si le type de number n’était déterminé qu’à l’exécution ; le compilateur serait plus complexe et offrirait moins de garanties sur le code s’il devait suivre plusieurs types hypothétiques pour chaque variable.
La répétition avec les boucles
Il est souvent utile d’exécuter un bloc de code plus d’une fois. Pour cette tâche, Rust fournit plusieurs boucles, qui exécuteront le code à l’intérieur du corps de la boucle jusqu’à la fin, puis recommenceront immédiatement au début. Pour expérimenter avec les boucles, créons un nouveau projet appelé loops.
Rust dispose de trois types de boucles : loop, while et for. Essayons chacune d’entre elles.
Répéter du code avec loop
Le mot-clé loop indique à Rust d’exécuter un bloc de code encore et encore, soit indéfiniment, soit jusqu’à ce que vous lui disiez explicitement de s’arrêter.
À titre d’exemple, modifiez le fichier src/main.rs dans votre répertoire loops pour qu’il ressemble à ceci :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
loop {
println!("again!");
}
}
Lorsque nous exécutons ce programme, nous verrons again! s’afficher en continu jusqu’à ce que nous arrêtions le programme manuellement. La plupart des terminaux prennent en charge le raccourci clavier ctrl-C pour interrompre un programme bloqué dans une boucle continue. Essayez :
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
Le symbole ^C représente l’endroit où vous avez appuyé sur ctrl-C.
Vous verrez peut-être ou non le mot again! affiché après le ^C, selon l’endroit où le code se trouvait dans la boucle lorsqu’il a reçu le signal d’interruption.
Heureusement, Rust fournit également un moyen de sortir d’une boucle par le code. Vous pouvez placer le mot-clé break à l’intérieur de la boucle pour indiquer au programme quand arrêter l’exécution de la boucle. Rappelez-vous que nous avons fait cela dans le jeu de devinettes dans la section [“Quitting After a Correct Guess”][quitting-after-a-correct-guess] du chapitre 2 pour quitter le programme lorsque l’utilisateur gagnait le jeu en devinant le bon nombre.
Nous avons également utilisé continue dans le jeu de devinettes, qui dans une boucle indique au programme de passer tout le code restant dans cette itération de la boucle et de passer à l’itération suivante.
Retourner des valeurs depuis les boucles
L’une des utilisations d’une boucle loop est de réessayer une opération dont vous savez qu’elle pourrait échouer, comme vérifier si un thread a terminé son travail. Vous pourriez également avoir besoin de transmettre le résultat de cette opération hors de la boucle au reste de votre code. Pour ce faire, vous pouvez ajouter la valeur que vous souhaitez retourner après l’expression break que vous utilisez pour arrêter la boucle ; cette valeur sera retournée hors de la boucle afin que vous puissiez l’utiliser, comme montré ici : rust {{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-33-return-value-from-loop/src/main.rs}}
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
Avant la boucle, nous déclarons une variable nommée counter et l’initialisons à 0. Puis, nous déclarons une variable nommée result pour contenir la valeur retournée par la boucle. À chaque itération de la boucle, nous ajoutons 1 à la variable counter, puis vérifions si counter est égal à 10. Quand c’est le cas, nous utilisons le mot-clé break avec la valeur counter * 2. Après la boucle, nous utilisons un point-virgule pour terminer l’instruction qui assigne la valeur à result. Enfin, nous affichons la valeur dans result, qui dans ce cas est 20.
Vous pouvez également utiliser return depuis l’intérieur d’une boucle. Alors que break ne quitte que la boucle en cours, return quitte toujours la fonction en cours.
Lever l’ambiguïté avec les étiquettes de boucle
Si vous avez des boucles à l’intérieur de boucles, break et continue s’appliquent à la boucle la plus interne à ce moment-là. Vous pouvez optionnellement spécifier une étiquette de boucle (loop label) sur une boucle que vous pouvez ensuite utiliser avec break ou continue pour spécifier que ces mots-clés s’appliquent à la boucle étiquetée plutôt qu’à la boucle la plus interne. Les étiquettes de boucle doivent commencer par une apostrophe. Voici un exemple avec deux boucles imbriquées : rust {{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-5-loop-labels/src/main.rs}}
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
La boucle extérieure à l’étiquette 'counting_up, et elle comptera de 0 à 2. La boucle intérieure sans étiquette compte à rebours de 10 à 9. Le premier break qui ne spécifie pas d’étiquette ne quittera que la boucle intérieure. L’instruction break 'counting_up; quittera la boucle extérieure. Ce code affiche : console {{#rustdoc_include ../listings/ch03-common-programming-concepts/no-listing-32-5-loop-labels/output.txt}}
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
Simplifier les boucles conditionnelles avec while
Un programme aura souvent besoin d’évaluer une condition à l’intérieur d’une boucle. Tant que la condition est true, la boucle s’exécute. Lorsque la condition cesse d’être true, le programme appelle break, arrêtant la boucle. Il est possible d’implémenter ce comportement en utilisant une combinaison de loop, if, else et break ; vous pourriez essayer cela maintenant dans un programme, si vous le souhaitez. Cependant, ce patron est si courant que Rust dispose d’une construction de langage intégrée pour cela, appelée boucle while. Dans le listing 3-3, nous utilisons while pour boucler le programme trois fois, en comptant à rebours à chaque fois, puis, après la boucle, pour afficher un message et quitter.
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
while loop to run code while a condition evaluates to trueCette construction élimine beaucoup d’imbrication qui serait nécessaire si vous utilisiez loop, if, else et break, et elle est plus claire. Tant qu’une condition s’évalue à true, le code s’exécute ; sinon, il quitte la boucle.
Parcourir une collection avec for
Vous pouvez choisir d’utiliser la construction while pour parcourir les éléments d’une collection, comme un tableau. Par exemple, la boucle dans le listing 3-4 affiche chaque élément du tableau a.
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
while loopIci, le code parcourt les éléments du tableau en incrémentant. Il commence à l’indice 0 puis boucle jusqu’à atteindre le dernier indice du tableau (c’est- à-dire quand index < 5 n’est plus true). L’exécution de ce code affichera chaque élément du tableau : console {{#include ../listings/ch03-common-programming-concepts/listing-03-04/output.txt}}
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
Les cinq valeurs du tableau apparaissent dans le terminal, comme prévu. Même si index atteindra la valeur 5 à un moment donné, la boucle s’arrête de s’exécuter avant d’essayer de récupérer une sixième valeur du tableau.
Cependant, cette approche est sujette aux erreurs ; nous pourrions provoquer un panic du programme si la valeur de l’indice ou la condition de test est incorrecte. Par exemple, si vous changiez la définition du tableau a pour avoir quatre éléments mais oubliiez de mettre à jour la condition en while index < 4, le code provoquerait un panic. C’est également lent, car le compilateur ajouté du code à l’exécution pour vérifier si l’indice est dans les limites du tableau à chaque itération de la boucle.
Comme alternative plus concise, vous pouvez utiliser une boucle for et exécuter du code pour chaque élément d’une collection. Une boucle for ressemble au code du listing 3-5.
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
for loopLorsque nous exécutons ce code, nous verrons la même sortie que dans le listing 3-4. Plus important encore, nous avons maintenant augmenté la sécurité du code et éliminé le risque de bugs qui pourraient résulter d’un dépassement de la fin du tableau ou d’un parcours insuffisant manquant certains éléments. Le code machine généré par les boucles for peut également être plus efficace car l’indice n’a pas besoin d’être comparé à la longueur du tableau à chaque itération.
En utilisant la boucle for, vous n’auriez pas besoin de vous souvenir de modifier d’autre code si vous changiez le nombre de valeurs dans le tableau, comme ce serait le cas avec la méthode utilisée dans le listing 3-4.
La sécurité et la concision des boucles for en font la construction de boucle la plus couramment utilisée en Rust. Même dans les situations où vous voulez exécuter du code un certain nombre de fois, comme dans l’exemple du compte à rebours qui utilisait une boucle while dans le listing 3-3, la plupart des Rustaceans utiliseraient une boucle for. La façon de le faire serait d’utiliser un Range, fourni par la bibliothèque standard, qui génère tous les nombres en séquence en commençant par un nombre et en s’arrêtant avant un autre nombre.
Voici à quoi ressemblerait le compte à rebours en utilisant une boucle for et une autre méthode dont nous n’avons pas encore parlé, rev, pour inverser l’intervalle :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
Ce code est un peu plus élégant, n’est-ce pas ?
Résumé
Vous y êtes arrivé ! C’était un chapitre conséquent : vous avez appris les variables, les types de données scalaires et composés, les fonctions, les commentaires, les expressions if et les boucles ! Pour vous entraîner avec les concepts abordés dans ce chapitre, essayez de construire des programmes pour faire ce qui suit :
- Convertir des températures entre Fahrenheit et Celsius.
- Générer le n-ième nombre de Fibonacci.
- Afficher les paroles du chant de Noël “The Twelve Days of Christmas”, en tirant parti de la répétition dans la chanson.
Quand vous serez prêt à continuer, nous parlerons d’un concept en Rust qui n’existe pas couramment dans les autres langages de programmation : l’ownership (la possession).