Les structures de contrôle

Pouvoir exécuter ou non du code si une condition est vérifiée, ou exécuter du code de façon répétée tant qu'une condition est vérifiée, sont des constructions élémentaires dans la plupart des langages de programmation. Les structures de contrôle les plus courantes en Rust sont les expressions if et les boucles.

Les expressions if

Une expression if vous permet de diviser votre code en fonction de conditions. Vous précisez une condition et vous choisissez ensuite : “Si cette condition est remplie, alors exécuter ce bloc de code. Si la condition n'est pas remplie, ne pas exécuter ce bloc de code.”

Créez un nouveau projet appelé branches dans votre dossier projects pour découvrir les expressions if. Dans le fichier src/main.rs, écrivez ceci :

Fichier : src/main.rs

fn main() {
    let nombre = 3;

    if nombre < 5 {
        println!("La condition est vérifiée");
    } else {
        println!("La condition n'est pas vérifiée");
    }
}

Une expression if commence par le mot-clé if, suivi d'une condition. Dans notre cas, la condition vérifie si oui ou non la variable nombre a une valeur inférieure à 5. Nous ajoutons le bloc de code à exécuter si la condition est vérifiée immédiatement après la condition entre des accolades. Les blocs de code associés à une condition dans une expression if sont parfois appelés des branches, exactement comme les branches dans les expressions match que nous avons vu dans la section “Comparer le nombre saisi au nombre secret” du chapitre 2.

Éventuellement, vous pouvez aussi ajouter une expression else, ce que nous avons fait ici, pour préciser un bloc alternatif de code qui sera exécuté dans le cas où la condition est fausse (elle n'est pas vérifiée). Si vous ne renseignez pas d'expression else et que la condition n'est pas vérifiée, le programme va simplement sauter le bloc de if et passer au prochain morceau de code.

Essayez d'exécuter ce code ; vous verrez ceci :

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
La condition est vérifiée

Essayons de changer la valeur de nombre pour une valeur qui rend la condition non vérifiée pour voir ce qui se passe :

fn main() {
    let nombre = 7;

    if nombre < 5 {
        println!("La condition est vérifiée");
    } else {
        println!("La condition n'est pas vérifiée");
    }
}

Exécutez à nouveau le programme, et regardez le résultat :

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
La condition n'est pas vérifiée

Il est aussi intéressant de noter que la condition dans ce code doit être un bool. Si la condition n'est pas un bool, nous aurons une erreur. Par exemple, essayez d'exécuter le code suivant :

Fichier : src/main.rs

fn main() {
    let nombre = 3;

    if nombre {
        println!("Le nombre était trois");
    }
}

La condition if vaut 3 cette fois, et Rust lève une erreur :

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if nombre {
  |        ^^^^^^ expected bool, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

Cette erreur explique que Rust attendait un bool mais a obtenu un entier (integer). Contrairement à des langages comme Ruby et JavaScript, Rust ne va pas essayer de convertir automatiquement les types non booléens en booléens. Vous devez être précis et toujours fournir un booléen à la condition d'un if. Si nous voulons que le bloc de code du if soit exécuté quand le nombre est différent de 0, par exemple, nous pouvons changer l'expression if par la suivante :

Fichier: src/main.rs

fn main() {
    let nombre = 3;

    if nombre != 0 {
        println!("Le nombre valait autre chose que zéro");
    }
}

Exécuter ce code va bien afficher Le nombre valait autre chose que 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 :

Fichier : src/main.rs

fn main() {
    let nombre = 6;

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

Ce programme peut choisir entre quatre chemins différents. Après l'avoir exécuté, vous devriez voir le résultat suivant :

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
Le nombre est divisible par 3

Quand ce programme s'exécute, il vérifie chaque expression if à tour de rôle et exécute le premier bloc dont la condition est vérifiée. Notez que même si 6 est divisible par 2, nous ne voyons pas le message Le nombre est divisible par 2, ni le message Le nombre n'est pas divisible par 4, 3 ou 2 du bloc else. C'est parce que Rust n'exécute que le bloc de la première condition vérifiée, et dès lors qu'il en a trouvé une, il ne va pas chercher à vérifier les suivantes.

Utiliser trop d'expressions else if peut encombrer votre code, donc si vous en avez plus d'une, vous devriez envisager de remanier votre code. Le chapitre 6 présente une construction puissante appelée match pour de tels cas.

Utiliser if dans une instruction let

Comme if est une expression, nous pouvons l'utiliser à droite d'une instruction let pour assigner le résultat à une variable, comme dans l'encart 3-2.

Fichier : src/main.rs

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

    println!("La valeur du nombre est : {}", nombre);
}

Encart 3-2 : assigner le résultat d'une expression if à une variable

La variable nombre va avoir la valeur du résultat de l'expression if. Exécutez ce code pour découvrir ce qui va se passer :

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

Souvenez-vous que les blocs de code s'exécutent jusqu'à la dernière expression qu'ils contiennent, et que les nombres tout seuls sont aussi des expressions. Dans notre cas, la valeur de toute l'expression if dépend de quel bloc de code elle va exécuter. Cela veut dire que chaque valeur qui peut être le résultat de chaque branche du if doivent être du même type ; dans l'encart 3-2, les résultats des branches if et else sont tous deux des entiers i32. Si les types ne sont pas identiques, comme dans l'exemple suivant, nous allons obtenir une erreur :

Fichier : src/main.rs

fn main() {
    let condition = true;

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

    println!("La valeur du nombre est : {}", nombre);
}

Lorsque nous essayons de compiler ce code, nous obtenons une erreur. Les branches if et else ont des types de valeurs qui ne sont pas compatibles, et Rust indique exactement où trouver le problème dans le programme :

$ 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 nombre = 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` due to previous error

L'expression dans le bloc if donne un entier, et l'expression dans le bloc else donne une chaîne de caractères. Ceci ne fonctionne pas car les variables doivent avoir un seul type, et Rust a besoin de savoir de quel type est la variable nombre au moment de la compilation. Savoir le type de nombre permet au compilateur de vérifier que le type est valable n'importe où nous utilisons nombre. Rust ne serait pas capable de faire cela si le type de nombre était déterminé uniquement à l'exécution ; car le compilateur deviendrait plus complexe et nous donnerait moins de garanties sur le code s'il devait prendre en compte tous les types hypothétiques pour une variable.

Les répétitions avec les boucles

Il est parfois utile d'exécuter un bloc de code plus d'une seule fois. Dans ce but, Rust propose plusieurs types de boucles, qui parcourt le code à l'intérieur du corps de la boucle jusqu'à la fin et recommence immédiatement du début. Pour tester les boucles, créons un nouveau projet appelé loops.

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

Répéter du code avec loop

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

Par exemple, changez le fichier src/main.rs dans votre dossier loops comme ceci :

Fichier : src/main.rs

fn main() {
    loop {
        println!("À nouveau !");
    }
}

Quand nous exécutons ce programme, nous voyons À nouveau ! s'afficher encore et encore en continu jusqu'à ce qu'on arrête le programme manuellement. La plupart des terminaux utilisent un raccourci clavier, ctrl-c, pour arrêter un programme qui est bloqué dans une boucle infinie. Essayons cela :

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
À nouveau !
À nouveau !
À nouveau !
À nouveau !
^CÀ nouveau !

Le symbole ^C représente le moment où vous avez appuyé sur ctrl-c. Vous devriez voir ou non le texte À nouveau ! après le ^C, en fonction de là où la boucle en était dans votre code quand elle a reçu le signal d'arrêt.

Heureusement, Rust fournit aussi un autre moyen de sortir d'une boucle en utilisant du code. Vous pouvez ajouter le mot-clé break à l'intérieur de la boucle pour demander au programme d'arrêter la boucle. Souvenez-vous que nous avions fait ceci dans le jeu de devinettes, dans la section “Arrêter le programme après avoir gagné” du chapitre 2 afin de quitter le programme quand l'utilisateur gagne le jeu en devinant le bon nombre.

Nous avons également continue dans le jeu du plus ou du moins, qui dans une boucle demande au programme de sauter le code restant dans cette iteration de la boucle et passer directement à la prochaine itération.

Si vous avez des boucles imbriquées dans d'autres boucles, break et continue s'appliquent uniquement à la boucle au plus bas niveau. Si vous en avez besoin, vous pouvez associer une etiquette de boucle à une boucle que nous pouvons ensuite utiliser en association avec break ou continue pour préciser que ces mot-clés s'appliquent sur la boucle correspondant à l'étiquette plutôt qu'à la boucle la plus proche possible. Voici un exemple avec deux boucles imbriquées :

fn main() {
    let mut compteur = 0;
    'increment: loop {
        println!("compteur = {}", compteur);
        let mut restant = 10;

        loop {
            println!("restant = {}", restant);
            if restant == 9 {
                break;
            }
            if compteur == 2 {
                break 'increment;
            }
            restant -= 1;
        }

        compteur += 1;
    }
    println!("Fin du compteur = {}", compteur);
}

La boucle la plus à l'extérieur a l'étiquette increment, et elle va incrémenter de 0 à 2. La boucle à l'intérieur n'a pas d'étiquette et va décrementer de 10 à 9. Le premier break qui ne précise pas d'étiquette va arrêter uniquement la boucle interne. L'instruction break 'increment; va arrêter la boucle la plus à l'extérieur. Ce code va afficher :

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
compteur = 0
restant = 10
restant = 9
compteur = 1
restant = 10
restant = 9
compteur = 2
restant = 10
Fin du compteur = 2

Retourner des valeurs d'une boucle

L'une des utilisations d'une boucle loop est de réessayer une opération qui peut échouer, comme vérifier si une tâche a terminé son travail. Vous aurez aussi peut-être besoin de passer le résultat de l'opération au reste de votre code à l'extérieur de cette boucle. Pour ce faire, vous pouvez ajouter la valeur que vous voulez retourner après l'expression break que vous utilisez pour stopper la boucle ; cette valeur sera retournée à l'extérieur de la boucle pour que vous puissiez l'utiliser, comme ci-dessous :

fn main() {
    let mut compteur = 0;

    let resultat = loop {
        compteur += 1;

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

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

Avant la boucle, nous déclarons une variable avec le nom compteur et nous l'initialisons à 0. Ensuite, nous déclarons une variable resultat pour stocker la valeur retournée de la boucle. À chaque itération de la boucle, nous ajoutons 1 à la variable compteur, et ensuite nous vérifions si le compteur est égal à 10. Lorsque c'est le cas, nous utilisons le mot-clé break avec la valeur compteur * 2. Après la boucle, nous utilisons un point-virgule pour terminer l'instruction qui assigne la valeur à resultat. Enfin, nous affichons la valeur de resultat, qui est 20 dans ce cas-ci.

Les boucles conditionnelles avec while

Un programme a souvent besoin d'évaluer une condition dans une boucle. Tant que la condition est vraie, la boucle tourne. Quand la condition arrête d'être vraie, le programme appelle break, ce qui arrête la boucle. Il est possible d'implémenter un comportement comme celui-ci en combinant loop, if, else et break ; vous pouvez essayer de le faire, si vous voulez. Cependant, cette utilisation est si fréquente que Rust a une construction pour cela, intégrée dans le langage, qui s'appelle une boucle while. Dans l'encart 3-3, nous utilisons while pour boucler trois fois, en décrémentant à chaque fois, et ensuite, après la boucle, il va afficher un message et se fermer.

Fichier : src/main.rs

fn main() {
    let mut nombre = 3;

    while nombre != 0 {
        println!("{} !", nombre);

        nombre -= 1;
    }

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

Encart 3-3: utiliser une boucle while pour exécuter du code tant qu'une condition est vraie

Cette construction élimine beaucoup d'imbrications qui seraient nécessaires si vous utilisiez loop, if, else et break, et c'est aussi plus clair. Tant que la condition est vraie, le code est exécuté ; sinon, il quitte la boucle.

Boucler dans une collection avec for

Vous pouvez choisir d'utiliser la construction while pour itérer sur les éléments d'une collection, comme les tableaux. Par exemple, la boucle dans l'encart 3-4 affiche chaque élément présent dans le tableau a.

Fichier : src/main.rs

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

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

        indice += 1;
    }
}

Encart 3-4 : itération sur les éléments d'une collection en utilisant une boucle while

Ici, le code parcourt le tableau élément par élément. Il commence à l'indice 0, et ensuite boucle jusqu'à ce qu'il atteigne l'indice final du tableau (ce qui correspond au moment où la condition index < 5 n'est plus vraie). Exécuter ce code va afficher chaque élément du tableau :

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
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 s'affichent toutes dans le terminal, comme attendu. Même si indice va atteindre la valeur 5 à un moment, la boucle arrêtera de s'exécuter avant d'essayer de récupérer une sixième valeur du tableau.

Cependant, cette approche pousse à l'erreur ; nous pourrions faire paniquer le programme si la valeur de l'indice est trop grand ou que la condition du test est incorrecte. Par exemple, si vous changez la définition du tableau a pour avoir quatre éléments, mais que nous oublions de modifier la condition dans while indice < 4, le code paniquera. De plus, c'est lent, car le compilateur ajoute du code pour effectuer à l'exécution la vérification que l'indice est compris dans les limites du tableau, et cela à chaque itération de la boucle.

Pour une alternative plus concise, vous pouvez utiliser une boucle for et exécuter du code pour chaque élément dans une collection. Une boucle for s'utilise comme dans le code de l'encart 3-5.

Fichier : src/main.rs

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

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

Encart 3-5 : itérer sur chaque élément d'une collection en utilisant une boucle for

Lorsque nous exécutons ce code, nous obtenons les mêmes messages que dans l'encart 3-4. Mais ce qui est plus important, c'est que nous avons amélioré la sécurité de notre code et éliminé le risque de bogues qui pourraient survenir si on dépassait la fin du tableau, ou si on n'allait pas jusqu'au bout et qu'on ratait quelques éléments.

En utilisant la boucle for, vous n'aurez pas à vous rappeler de changer le code si vous changez le nombre de valeurs dans le tableau, comme vous devriez le faire dans la méthode utilisée dans l'encart 3-4.

La sécurité et la concision de la boucle for en font la construction de boucle la plus utilisée avec Rust. Même dans des situations dans lesquelles vous voudriez exécuter du code plusieurs fois, comme l'exemple du décompte qui utilisait une boucle while dans l'encart 3-3, la plupart des Rustacés utiliseraient une boucle for. Il faut pour cela utiliser un intervalle Range, fourni par la bibliothèque standard pour générer dans l'ordre tous les nombres compris entre un certain nombre et un autre nombre.

Voici ce que le décompte aurait donné en utilisant une boucle for et une autre méthode que nous n'avons pas encore vue, rev, qui inverse l'intervalle :

Fichier : src/main.rs

fn main() {
    for nombre in (1..4).rev() {
        println!("{} !", nombre);
    }
    println!("DÉCOLLAGE !!!");
}

Ce code est un peu plus sympa, non ?

Résumé

Vous y êtes arrivé ! C'était un chapitre important : vous avez appris les variables, les types scalaires et composés, les fonctions, les commentaires, les expressions if, et les boucles ! Pour pratiquer un peu les concepts abordés dans ce chapitre, voici quelques programmes que vous pouvez essayer de créer :

  • Convertir des températures entre les degrés Fahrenheit et Celsius.
  • Générer le n-ième nombre de Fibonacci.
  • Afficher les paroles de la chanson de Noël The Twelve Days of Christmas en profitant de l'aspect répétitif de la chanson.

Quand vous serez prêt à aller plus loin, nous aborderons une notion de Rust qui n'existe pas dans les autres langages de programmation : la possession (ownership).