Fondamentaux du contrôle de flux en Rust

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Control Flow. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons nous concentrer sur le contrôle du flux en Rust, qui consiste à utiliser des expressions if et des boucles pour exécuter du code en fonction de conditions et pour répéter du code tant qu'une condition est vraie.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") subgraph Lab Skills rust/variable_declarations -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/mutable_variables -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/boolean_type -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/string_type -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/for_loop -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/function_syntax -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} rust/expressions_statements -.-> lab-100391{{"Fondamentaux du contrôle de flux en Rust"}} end

Contrôle du flux

La capacité d'exécuter du code en fonction du fait qu'une condition est vraie et d'exécuter du code plusieurs fois tant qu'une condition est vraie sont des briques fondamentales 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.

Expressions if

Une expression if vous permet de créer des branches dans votre code en fonction de conditions. Vous fournissez une condition et ensuite vous indiquez : "Si cette condition est remplie, exécutez ce bloc de code. Si la condition n'est pas remplie, n'exécutez pas ce bloc de code."

Créez un nouveau projet appelé branches dans votre répertoire projet pour explorer l'expression if. Dans le fichier src/main.rs, entrez le code suivant :

cd ~/projet
cargo new branches

Nom de fichier : src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition était vraie");
    } else {
        println!("condition était fausse");
    }
}

Toutes les expressions if commencent par le mot-clé if, suivi d'une condition. Dans cet exemple, la condition vérifie si la variable number a une valeur inférieure à 5. Nous plaçons le bloc de code à exécuter si la condition est vraie immédiatement après la condition, à l'intérieur des accolades. Les blocs de code associés aux conditions dans les expressions if sont parfois appelés bras, tout comme les bras dans les expressions match que nous avons discutées dans "Comparer la proposition avec le nombre secret".

Facultativement, nous pouvons également inclure une expression else, comme nous l'avons fait ici, pour donner au programme un bloc de code alternatif à exécuter si la condition se révèle être fausse. Si vous ne fournissez pas d'expression else et que la condition est fausse, le programme saute simplement le bloc if et passe au reste du code.

Essayez d'exécuter ce code ; vous devriez voir la sortie suivante :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition était vraie

Essayons de modifier la valeur de number pour une valeur qui rend la condition fausse pour voir ce qui se passe :

    let number = 7;

Exécutez le programme à nouveau et regardez la sortie :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition était fausse

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 :

Nom de fichier : src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number était égal à trois");
    }
}

La condition if évalue à une valeur de 3 cette fois-ci, et Rust génère une erreur :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
error[E0308]: types non compatibles
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ valeur attendue : `bool`, valeur trouvée : entier

L'erreur indique que Rust attendait un bool mais a reçu un entier. Contrairement à des langages tels que Ruby et JavaScript, Rust ne tentera pas automatiquement de convertir des types non booléens en booléen. Vous devez être explicite et fournir toujours à if une condition de type booléen. Si nous voulons que le bloc de code if s'exécute seulement lorsqu'un nombre est différent de 0, par exemple, nous pouvons modifier l'expression if comme suit :

Nom de fichier : src/main.rs

fn main() {
    let number = 3;

    if number!= 0 {
        println!("number était différent de zéro");
    }
}

En exécutant ce code, il affichera number était différent de zéro.

Gérer plusieurs conditions avec else if

Vous pouvez utiliser plusieurs conditions en combinant if et else dans une expression else if. Par exemple :

Nom de fichier : src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number est divisible par 4");
    } else if number % 3 == 0 {
        println!("number est divisible par 3");
    } else if number % 2 == 0 {
        println!("number est divisible par 2");
    } else {
        println!("number n'est pas divisible par 4, 3 ou 2");
    }
}

Ce programme peut suivre quatre chemins possibles. Après l'avoir exécuté, vous devriez voir la sortie suivante :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number est divisible par 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 est évaluée à vraie. Notez que bien que 6 soit divisible par 2, nous ne voyons pas la sortie number est divisible par 2, ni le texte number n'est pas divisible par 4, 3 ou 2 du bloc else. C'est parce que Rust n'exécute le bloc que pour la première condition vraie, et une fois qu'il l'a trouvée, il ne vérifie même pas le reste.

L'utilisation de trop nombreuses expressions else if peut alourdir votre code, donc si vous en avez plus d'une, vous pouvez vouloir refactoriser votre code. Le chapitre 6 décrit une structure de branchement puissante en Rust appelée match pour ces cas.

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 la Liste 3-2.

Nom de fichier : src/main.rs

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("La valeur de number est : {number}");
}

Liste 3-2 : Affecter le résultat d'une expression if à une variable

La 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 :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
La valeur de number est : 5

Rappelez-vous que les blocs de code évaluent à la dernière expression qu'ils contiennent, et les nombres eux-mêmes sont également des expressions. Dans ce cas, la valeur de l'expression if entière dépend de quel bloc de code s'exécute. Cela signifie que les valeurs qui ont la possibilité d'être des résultats de chaque branche de l'if doivent être du même type ; dans la Liste 3-2, les résultats de la branche if et de la branche else étaient des entiers i32. Si les types ne correspondent pas, comme dans l'exemple suivant, nous obtiendrons une erreur :

Nom de fichier : src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("La valeur de number est : {number}");
}

Lorsque nous essayons de compiler ce code, nous obtiendrons 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 :

$ cargo run
   Compiling branches v0.1.0 (file:///projets/branches)
error[E0308]: `if` et `else` ont des types incompatibles
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ valeur attendue : entier, trouvée `&str`
  |                                 |
  |                                 pour cette raison

L'expression dans le bloc if évalue à un entier, et l'expression dans le bloc else évalue à une chaîne de caractères. Cela ne fonctionnera pas car les variables doivent avoir un seul type, et Rust doit savoir à la compilation quel type est la variable number, de manière définitive. Savoir le type de number permet au compilateur de vérifier que le type est valide partout où nous utilisons number. Rust ne serait pas capable de le faire si le type de number était seulement déterminé à 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 n'importe quelle variable.

Répétition avec des boucles

Il est souvent utile d'exécuter un bloc de code plusieurs fois. Pour cette tâche, Rust propose 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é boucles.

Rust a trois types de boucles : loop, while et for. Essayons chacun d'eux.

Répéter du code avec loop

Le mot-clé loop indique à Rust d'exécuter un bloc de code à l'infini ou jusqu'à ce que vous lui disiez explicitement de s'arrêter.

Par exemple, modifiez le fichier src/main.rs dans votre répertoire boucles pour qu'il ressemble à ceci :

Nom de fichier : src/main.rs

fn main() {
    loop {
        println!("encore!");
    }
}

Lorsque nous exécutons ce programme, nous verrons encore! affiché à l'infini 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 qui est bloqué dans une boucle infinie. Essayez-le :

$ cargo run
   Compiling boucles v0.1.0 (file:///projets/boucles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/boucles`
encore!
encore!
encore!
encore!
^Cencore!

Le symbole ^C représente le moment où vous avez appuyé sur ctrl-C. Vous pouvez ou non voir le mot encore! affiché après le ^C, selon 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 en utilisant du code. Vous pouvez placer le mot-clé break à l'intérieur de la boucle pour dire au programme quand arrêter d'exécuter la boucle. Rappelez-vous que nous avons fait cela dans le jeu de devinette dans "Quitter après avoir deviné correctement" pour quitter le programme lorsque l'utilisateur a gagné le jeu en devinant le bon nombre.

Nous avons également utilisé continue dans le jeu de devinette, qui dans une boucle indique au programme de sauter le reste du code dans cette itération de la boucle et de passer à l'itération suivante.

Retourner des valeurs à partir de boucles

L'une des utilisations d'une boucle loop est de réessayer une opération que vous savez peut échouer, comme vérifier si un thread a terminé son travail. Vous pourriez également avoir besoin de passer le résultat de cette opération en dehors de la boucle pour le reste de votre code. Pour ce faire, vous pouvez ajouter la valeur que vous voulez retourner après l'expression break que vous utilisez pour arrêter la boucle ; cette valeur sera retournée en dehors de la boucle pour que vous puissiez l'utiliser, comme le montre ici :

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("Le résultat est {result}");
}

Avant la boucle, nous déclarons une variable nommée counter et l'initialisons à 0. Ensuite, nous déclarons une variable nommée result pour stocker la valeur retournée par la boucle. A chaque itération de la boucle, nous ajoutons 1 à la variable counter, puis vérifions si counter est égal à 10. Lorsque 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 attribue la valeur à result. Enfin, nous affichons la valeur de result, qui est dans ce cas 20.

Etiquettes de boucle pour désambiguïser entre plusieurs boucles

Si vous avez des boucles imbriquées, break et continue s'appliquent à la boucle la plus interne à ce moment-là. Vous pouvez optionnellement spécifier une étiquette de boucle 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 simple apostrophe. Voici un exemple avec deux boucles imbriquées :

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 externe a l'étiquette 'counting_up, et elle comptera de 0 à 2. La boucle interne sans étiquette compte à rebours de 10 à 9. Le premier break qui ne spécifie pas d'étiquette ne sortira que de la boucle interne. L'instruction break 'counting_up; sortira de la boucle externe. Ce code affiche :

   Compiling boucles v0.1.0 (file:///projets/boucles)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/boucles`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

Boucles conditionnelles avec while

Un programme a souvent besoin d'évaluer une condition à l'intérieur d'une boucle. Tant que la condition est vraie, la boucle s'exécute. Lorsque la condition cesse d'être vraie, le programme appelle break, ce qui arrête la boucle. Il est possible de mettre en œuvre un comportement de ce type en utilisant une combinaison de loop, if, else et break ; vous pouvez essayer cela maintenant dans un programme, si vous le souhaitez. Cependant, ce modèle est si courant que Rust a une construction de langage intégrée pour cela, appelée boucle while. Dans la liste 3-3, nous utilisons while pour faire boucler le programme trois fois, en décomptant à chaque fois, puis, après la boucle, afficher un message et sortir.

Nom de fichier : src/main.rs

fn main() {
    let mut number = 3;

    while number!= 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("DÉCOLAGE!!!");
}

Liste 3-3 : Utilisation d'une boucle while pour exécuter du code tant qu'une condition est évaluée comme vraie

Cette construction élimine beaucoup d'imbrications qui seraient nécessaires si vous utilisiez loop, if, else et break, et elle est plus claire. Tant qu'une condition est évaluée comme vraie, le code s'exécute ; sinon, il sort de 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 la liste 3-4 affiche chaque élément du tableau a.

Nom de fichier : src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("la valeur est : {}", a[index]);

        index += 1;
    }
}

Liste 3-4 : Parcourir chaque élément d'une collection en utilisant une boucle while

Ici, le code compte les éléments du tableau. Il commence à l'index 0, puis boucle jusqu'à ce qu'il atteigne le dernier index du tableau (c'est-à-dire lorsque index < 5 n'est plus vrai). En exécutant ce code, tous les éléments du tableau seront affichés :

$ cargo run
   Compilant boucles v0.1.0 (file:///projets/boucles)
    Terminé en mode développement [non optimisé + débogage] cibles(s) en 0.32s
     Exécution `target/debug/boucles`
la valeur est : 10
la valeur est : 20
la valeur est : 30
la valeur est : 40
la valeur est : 50

Les cinq valeurs du tableau apparaissent dans le terminal, comme attendu. Même si index atteindra une valeur de 5 à un moment donné, la boucle s'arrête avant d'essayer de récupérer une sixième valeur dans le tableau.

Cependant, cette approche est propice à des erreurs ; nous pourrions faire planter le programme si la valeur de l'index ou la condition de test est incorrecte. Par exemple, si vous changez la définition du tableau a pour qu'il ait quatre éléments mais oubliez de mettre à jour la condition en while index < 4, le code plantera. Elle est également lente, car le compilateur ajoute du code exécuté pendant l'exécution pour effectuer la vérification conditionnelle de savoir si l'index est dans les limites du tableau à chaque itération de la boucle.

En guise d'alternative plus concise, vous pouvez utiliser une boucle for et exécuter un certain code pour chaque élément d'une collection. Une boucle for ressemble au code de la liste 3-5.

Nom de fichier : src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("la valeur est : {element}");
    }
}

Liste 3-5 : Parcourir chaque élément d'une collection en utilisant une boucle for

Lorsque nous exécutons ce code, nous verrons la même sortie que dans la liste 3-4. Plus important, nous avons maintenant augmenté la sécurité du code et éliminé la possibilité de bugs qui pourraient résulter de dépasser la fin du tableau ou de ne pas aller assez loin et manquer certains éléments.

En utilisant la boucle for, vous n'aurez pas besoin de vous souvenir de changer tout autre code si vous changez le nombre de valeurs dans le tableau, contrairement à la méthode utilisée dans la liste 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 des situations où vous voulez exécuter un certain code un certain nombre de fois, comme dans l'exemple de décompte qui utilisait une boucle while dans la liste 3-3, la plupart des Rustaceens 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 séquentiellement, en commençant par un nombre et en terminant avant un autre nombre.

Voici à quoi ressemblerait le décompte en utilisant une boucle for et une autre méthode dont nous n'avons pas encore parlé, rev, pour inverser la plage :

Nom de fichier : src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("DÉCOLAGE!!!");
}

Ce code est un peu plus agréable, non?

Sommaire

Félicitations! Vous avez terminé le laboratoire de Contrôle de flux. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.