Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Programmer un jeu de devinettes

Plongeons dans Rust en travaillant ensemble sur un projet concret ! Ce chapitre vous présente quelques concepts courants de Rust en vous montrant comment les utiliser dans un vrai programme. Vous découvrirez let, match, les méthodes, les fonctions associées, les crates externes, et bien plus ! Dans les chapitres suivants, nous explorerons ces notions plus en détail. Dans ce chapitre, vous pratiquerez simplement les bases.

Nous allons implémenter un problème classique de programmation pour débutants : un jeu de devinettes. Voici comment il fonctionne : le programme génère un nombre entier aléatoire entre 1 et 100. Il demande ensuite au joueur de saisir une proposition. Après chaque saisie, le programme indique si la proposition est trop basse ou trop haute. Si la proposition est correcte, le jeu affiche un message de félicitations et se terminé.

Mise en place d’un nouveau projet

Pour mettre en place un nouveau projet, rendez-vous dans le répertoire projects que vous avez créé au chapitre 1 et créez un nouveau projet avec Cargo, comme ceci :

$ cargo new guessing_game
$ cd guessing_game

La première commande, cargo new, prend le nom du projet (guessing_game) comme premier argument. La seconde commande se déplace dans le répertoire du nouveau projet.

Regardons le fichier Cargo.toml généré :

Fichier : Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"

[dependencies]

Comme vous l’avez vu au chapitre 1, cargo new génère un programme “Hello, world!” pour vous. Regardons le fichier src/main.rs : Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-01-cargo-new/src/main.rs}}

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() {
    println!("Hello, world!");
}

Maintenant, compilons ce programme “Hello, world!” et exécutons-le en une seule étape avec la commande cargo run : console {{#include ../listings/ch02-guessing-game-tutorial/no-listing-01-cargo-new/output.txt}}

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/guessing_game`
Hello, world!

La commande run est pratique lorsque vous avez besoin d’itérer rapidement sur un projet, comme nous le ferons dans ce jeu, en testant rapidement chaque itération avant de passer à la suivante.

Rouvrez le fichier src/main.rs. Vous écrirez tout le code dans ce fichier.

Traitement d’une proposition

La première partie du programme du jeu de devinettes demandera une saisie à l’utilisateur, traitera cette saisie et vérifiera qu’elle est dans la forme attendue. Pour commencer, nous allons permettre au joueur de saisir une proposition. Entrez le code de l’encart 2-1 dans src/main.rs.

Filename: src/main.rs
use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-1: Code that gets a guess from the user and prints it

Ce code contient beaucoup d’informations, alors parcourons-le ligne par ligne. Pour obtenir la saisie de l’utilisateur puis afficher le résultat en sortie, nous devons importer la bibliothèque d’entrée/sortie io dans la portée. La bibliothèque io provient de la bibliothèque standard, connue sous le nom de std : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:io}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Par défaut, Rust dispose d’un ensemble d’éléments définis dans la bibliothèque standard qu’il importe dans la portée de chaque programme. Cet ensemble s’appelle le prelude, et vous pouvez voir tout ce qu’il contient [dans la documentation de la bibliothèque standard][prelude].

Si un type que vous souhaitez utiliser n’est pas dans le prelude, vous devez l’importer explicitement dans la portée avec une instruction use. Utiliser la bibliothèque std::io vous fournit un certain nombre de fonctionnalités utiles, notamment la possibilité d’accepter des saisies utilisateur.

Comme vous l’avez vu au chapitre 1, la fonction main est le point d’entrée du programme : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:main}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

La syntaxe fn déclare une nouvelle fonction ; les parenthèses, (), indiquent qu’il n’y a pas de paramètres ; et l’accolade ouvrante, {, commence le corps de la fonction.

Comme vous l’avez également appris au chapitre 1, println! est une macro qui affiche une chaîne de caractères à l’écran : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Ce code affiche un message indiquant de quoi il s’agit et demande une saisie à l’utilisateur.

Stocker des valeurs avec des variables

Ensuite, nous allons créer une variable pour stocker la saisie de l’utilisateur, comme ceci : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:string}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Maintenant le programme devient intéressant ! Il se passe beaucoup de choses dans cette petite ligne. Nous utilisons l’instruction let pour créer la variable. Voici un autre exemple :

let apples = 5;

section in Chapter 3. To make a variable mutable, we add mut before the variable name: –> Cette ligne crée une nouvelle variable nommée apples et la lie à la valeur 5. En Rust, les variables sont immuables par défaut, ce qui signifie qu’une fois que nous avons donné une valeur à la variable, la valeur ne changera pas. Nous discuterons de ce concept en détail dans la section [“Variables et mutabilité”][variables-and-mutability] du chapitre 3. Pour rendre une variable mutable, nous ajoutons mut devant le nom de la variable :

let apples = 5; // immutable
let mut bananas = 5; // mutable

Remarque : la syntaxe // commence un commentaire qui se poursuit jusqu’à la fin de la ligne. Rust ignore tout ce qui se trouve dans les commentaires. Nous discuterons des commentaires plus en détail au [chapitre 3][comments].

En revenant au programme du jeu de devinettes, vous savez maintenant que let mut guess introduit une variable mutable nommée guess. Le signé égal (=) indique à Rust que nous voulons lier quelque chose à la variable maintenant. À droite du signé égal se trouve la valeur à laquelle guess est liée, qui est le résultat de l’appel à String::new, une fonction qui renvoie une nouvelle instance de String. [String][string] est un type de chaîne de caractères fourni par la bibliothèque standard, qui est un texte extensible encodé en UTF-8.

La syntaxe :: dans la ligne ::new indique que new est une fonction associée du type String. Une fonction associée est une fonction qui est implémentée sur un type, dans ce cas String. Cette fonction new crée une nouvelle chaîne de caractères vide. Vous trouverez une fonction new sur de nombreux types car c’est un nom courant pour une fonction qui crée une nouvelle valeur d’un certain type.

En résumé, la ligne let mut guess = String::new(); a créé une variable mutable qui est actuellement liée à une nouvelle instance vide de String. Ouf !

Recevoir la saisie de l’utilisateur

Rappelons que nous avons inclus la fonctionnalité d’entrée/sortie de la bibliothèque standard avec use std::io; sur la première ligne du programme. Maintenant, nous allons appeler la fonction stdin du module io, qui nous permettra de gérer la saisie de l’utilisateur : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:read}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Si nous n’avions pas importé le module io avec use std::io; au début du programme, nous pourrions quand même utiliser la fonction en écrivant l’appel comme std::io::stdin. La fonction stdin renvoie une instance de [std::io::Stdin][iostdin], qui est un type représentant un descripteur vers l’entrée standard de votre terminal.

Ensuite, la ligne .read_line(&mut guess) appelle la méthode read_line sur le descripteur d’entrée standard pour obtenir la saisie de l’utilisateur. Nous passons également &mut guess comme argument à read_line pour lui indiquer dans quelle chaîne stocker la saisie de l’utilisateur. Le rôle complet de read_line est de prendre tout ce que l’utilisateur saisit dans l’entrée standard et de l’ajouter à une chaîne (sans écraser son contenu), donc nous passons cette chaîne en argument. L’argument chaîne doit être mutable pour que la méthode puisse modifier le contenu de la chaîne.

Le & indique que cet argument est une référence, qui vous permet de laisser plusieurs parties de votre code accéder à une même donnée sans avoir besoin de copier cette donnée en mémoire plusieurs fois. Les références sont une fonctionnalité complexe, et l’un des avantages majeurs de Rust est la sécurité et la facilité d’utilisation des références. Vous n’avez pas besoin de connaître tous ces détails pour terminer ce programme. Pour l’instant, tout ce que vous devez savoir, c’est que, comme les variables, les références sont immuables par défaut. Par conséquent, vous devez écrire &mut guess plutôt que &guess pour la rendre mutable. (Le chapitre 4 expliquera les références plus en détail.)

Gérer les erreurs potentielles avec Result

Nous travaillons toujours sur cette ligne de code. Nous discutons maintenant d’une troisième ligne de texte, mais notez qu’elle fait toujours partie d’une seule ligne logique de code. La partie suivante est cette méthode : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:expect}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Nous aurions pu écrire ce code ainsi :

io::stdin().read_line(&mut guess).expect("Failed to read line");

Cependant, une seule longue ligne est difficile à lire, il est donc préférable de la diviser. Il est souvent judicieux d’introduire un retour à la ligne et d’autres espaces pour aérer les longues lignes lorsque vous appelez une méthode avec la syntaxe .nom_de_methode(). Voyons maintenant ce que fait cette ligne.

Comme mentionné précédemment, read_line place ce que l’utilisateur saisit dans la chaîne que nous lui passons, mais elle renvoie également une valeur Result. [Result][result] est une [énumération][enums], souvent appelée enum, qui est un type pouvant se trouver dans l’un de plusieurs états possibles. Nous appelons chaque état possible une variante.

Le chapitre 6 couvrira les enums plus en détail. L’objectif de ces types Result est d’encoder les informations de gestion d’erreurs.

Les variantes de Result sont Ok et Err. La variante Ok indique que l’opération a réussi et contient la valeur produite avec succès. La variante Err signifie que l’opération a échoué et contient des informations sur la manière ou la raison de l’échec.

that you can call. If this instance of Result is an Err value, expect will cause the program to crash and display the message that you passed as an argument to expect. If the read_line method returns an Err, it would likely be the result of an error coming from the underlying operating system. If this instance of Result is an Ok value, expect will take the return value that Ok is holding and return just that value to you so that you can use it. In this case, that value is the number of bytes in the user’s input. –> Les valeurs du type Result, comme les valeurs de tout type, ont des méthodes définies sur elles. Une instance de Result possède une [méthode expect][expect] que vous pouvez appeler. Si cette instance de Result est une valeur Err, expect provoquera le plantage du programme et affichera le message que vous avez passé en argument à expect. Si la méthode read_line renvoie un Err, il s’agirait probablement d’une erreur provenant du système d’exploitation sous-jacent. Si cette instance de Result est une valeur Ok, expect prendra la valeur de retour contenue dans Ok et vous renverra uniquement cette valeur afin que vous puissiez l’utiliser. Dans ce cas, cette valeur est le nombre d’octets dans la saisie de l’utilisateur.

Si vous n’appelez pas expect, le programme compilera, mais vous obtiendrez un avertissement : console {{#include ../listings/ch02-guessing-game-tutorial/no-listing-02-without-expect/output.txt}}

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
10 |     let _ = io::stdin().read_line(&mut guess);
   |     +++++++

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s

Rust vous avertit que vous n’avez pas utilisé la valeur Result renvoyée par read_line, indiquant que le programme n’a pas géré une erreur possible.

La bonne façon de supprimer l’avertissement est d’écrire réellement du code de gestion d’erreurs, mais dans notre cas, nous voulons simplement que le programme plante lorsqu’un problème survient, donc nous pouvons utiliser expect. Vous apprendrez à récupérer des erreurs au [chapitre 9][recover].

Afficher des valeurs avec les espaces réservés de println!

Mis à part l’accolade fermante, il ne reste qu’une seule ligne à discuter dans le code jusqu’ici : rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-01/src/main.rs:print_guess}}

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Cette ligne affiche la chaîne qui contient maintenant la saisie de l’utilisateur. Les accolades {} sont un espace réservé : pensez aux {} comme de petites pinces de crabe qui maintiennent une valeur en place. Lors de l’affichage de la valeur d’une variable, le nom de la variable peut être placé à l’intérieur des accolades. Pour afficher le résultat de l’évaluation d’une expression, placez des accolades vides dans la chaîne de format, puis faites suivre la chaîne de format d’une liste d’expressions séparées par des virgules à afficher dans chaque espace réservé dans le même ordre. Afficher une variable et le résultat d’une expression en un seul appel à println! ressemblerait à ceci :

#![allow(unused)]
fn main() {
let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);
}

Ce code afficherait x = 5 and y + 2 = 12.

Tester la première partie

Testons la première partie du jeu de devinettes. Exécutez-le avec cargo run :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

À ce stade, la première partie du jeu est terminée : nous récupérons la saisie au clavier puis nous l’affichons.

Générer un nombre secret

Ensuite, nous devons générer un nombre secret que l’utilisateur essaiera de deviner. Le nombre secret devrait être différent à chaque fois pour que le jeu reste amusant à rejouer. Nous utiliserons un nombre aléatoire entre 1 et 100 pour que le jeu ne soit pas trop difficile. Rust n’inclut pas encore de fonctionnalité de génération de nombres aléatoires dans sa bibliothèque standard. Cependant, l’équipe Rust fournit une [crate rand][randcrate] avec cette fonctionnalité.

Enrichir les fonctionnalités avec une crate

Rappelez-vous qu’une crate est une collection de fichiers de code source Rust. Le projet que nous construisons est une crate binaire, c’est-à-dire un exécutable. La crate rand est une crate de bibliothèque, qui contient du code destiné à être utilisé dans d’autres programmes et ne peut pas être exécutée seule.

La coordination des crates externes par Cargo est là où Cargo brille vraiment. Avant de pouvoir écrire du code qui utilise rand, nous devons modifier le fichier Cargo.toml pour inclure la crate rand comme dépendance. Ouvrez ce fichier maintenant et ajoutez la ligne suivante en bas, sous l’en-tête de section [dependencies] que Cargo a créé pour vous. Assurez-vous de spécifier rand exactement comme nous l’avons fait ici, avec ce numéro de version, sinon les exemples de code de ce tutoriel pourraient ne pas fonctionner :

Fichier : Cargo.toml

[dependencies]
rand = "0.8.5"

Dans le fichier Cargo.toml, tout ce qui suit un en-tête fait partie de cette section et continue jusqu’au début d’une autre section. Dans [dependencies], vous indiquez à Cargo de quelles crates externes votre projet dépend et quelles versions de ces crates vous exigez. Dans ce cas, nous spécifions la crate rand avec le spécificateur de version sémantique 0.8.5. Cargo comprend le [versionnement sémantique][semver] (parfois appelé SemVer), qui est un standard pour écrire les numéros de version. Le spécificateur 0.8.5 est en fait un raccourci pour ^0.8.5, ce qui signifie toute version supérieure ou égale à 0.8.5 mais inférieure à 0.9.0.

Cargo considère que ces versions ont des API publiques compatibles avec la version 0.8.5, et cette spécification garantit que vous obtiendrez la dernière version corrective qui compilera encore avec le code de ce chapitre. Toute version 0.9.0 ou supérieure n’est pas garantie d’avoir la même API que celle utilisée dans les exemples suivants.

Maintenant, sans modifier le code, construisons le projet, comme montré dans l’encart 2-2.

$ cargo build
  Updating crates.io index
   Locking 15 packages to latest Rust 1.85.0 compatible versions
    Adding rand v0.8.5 (available: v0.9.0)
 Compiling proc-macro2 v1.0.93
 Compiling unicode-ident v1.0.17
 Compiling libc v0.2.170
 Compiling cfg-if v1.0.0
 Compiling byteorder v1.5.0
 Compiling getrandom v0.2.15
 Compiling rand_core v0.6.4
 Compiling quote v1.0.38
 Compiling syn v2.0.98
 Compiling zerocopy-derive v0.7.35
 Compiling zerocopy v0.7.35
 Compiling ppv-lite86 v0.2.20
 Compiling rand_chacha v0.3.1
 Compiling rand v0.8.5
 Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
  Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
Listing 2-2: The output from running cargo build after adding the rand crate as a dependency

Vous pourriez voir des numéros de version différents (mais ils seront tous compatibles avec le code, grâce à SemVer !) et des lignes différentes (selon le système d’exploitation), et les lignes pourraient être dans un ordre différent.

Lorsque nous incluons une dépendance externe, Cargo récupère les dernières versions de tout ce dont cette dépendance a besoin depuis le registre, qui est une copie des données de [Crates.io][cratesio]. Crates.io est l’endroit où les membres de l’écosystème Rust publient leurs projets Rust open source pour que d’autres puissent les utiliser.

Après avoir mis à jour le registre, Cargo vérifie la section [dependencies] et télécharge toutes les crates listées qui ne sont pas encore téléchargées. Dans ce cas, bien que nous n’ayons listé que rand comme dépendance, Cargo a également récupéré d’autres crates dont rand dépend pour fonctionner. Après avoir téléchargé les crates, Rust les compilé puis compilé le projet avec les dépendances disponibles.

Si vous exécutez immédiatement cargo build à nouveau sans apporter de modifications, vous n’obtiendrez aucune sortie à part la ligne Finished. Cargo sait qu’il a déjà téléchargé et compilé les dépendances, et que vous n’avez rien changé dans votre fichier Cargo.toml. Cargo sait également que vous n’avez rien changé dans votre code, donc il ne le recompile pas non plus. N’ayant rien à faire, il se terminé simplement.

Si vous ouvrez le fichier src/main.rs, faites une modification mineure, puis sauvegardez-le et reconstruisez, vous ne verrez que deux lignes de sortie :

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s

Ces lignes montrent que Cargo ne met à jour la compilation qu’avec votre petite modification du fichier src/main.rs. Vos dépendances n’ont pas changé, donc Cargo sait qu’il peut réutiliser ce qu’il a déjà téléchargé et compilé pour celles-ci.

Garantir des compilations reproductibles

Cargo dispose d’un mécanisme qui garantit que vous pouvez reconstruire le même artefact à chaque fois que vous ou quelqu’un d’autre compilé votre code : Cargo n’utilisera que les versions des dépendances que vous avez spécifiées tant que vous n’indiquez pas le contraire. Par exemple, supposons que la semaine prochaine la version 0.8.6 de la crate rand sorte, et que cette version contienne un correctif important, mais aussi une régression qui casserait votre code. Pour gérer cela, Rust crée le fichier Cargo.lock la première fois que vous exécutez cargo build, donc nous avons maintenant ce fichier dans le répertoire guessing_game.

Lorsque vous compilez un projet pour la première fois, Cargo détermine toutes les versions des dépendances qui correspondent aux critères et les écrit dans le fichier Cargo.lock. Lorsque vous compilerez votre projet à l’avenir, Cargo verra que le fichier Cargo.lock existe et utilisera les versions spécifiées dedans plutôt que de refaire tout le travail de détermination des versions. Cela vous permet d’avoir une compilation reproductible automatiquement. En d’autres termes, votre projet restera en version 0.8.5 jusqu’à ce que vous fassiez une mise à jour explicite, grâce au fichier Cargo.lock. Comme le fichier Cargo.lock est important pour les compilations reproductibles, il est souvent versionné dans le contrôle de source avec le reste du code de votre projet.

Mettre à jour une crate pour obtenir une nouvelle version

Lorsque vous souhaitez effectivement mettre à jour une crate, Cargo fournit la commande update, qui ignorera le fichier Cargo.lock et déterminera toutes les dernières versions correspondant à vos spécifications dans Cargo.toml. Cargo écrira ensuite ces versions dans le fichier Cargo.lock. Par défaut, Cargo ne cherchera que les versions supérieures à 0.8.5 et inférieures à 0.9.0. Si la crate rand a publié deux nouvelles versions 0.8.6 et 0.999.0, vous verriez ce qui suit en exécutant cargo update :

$ cargo update
    Updating crates.io index
     Locking 1 package to latest Rust 1.85.0 compatible version
    Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)

Cargo ignore la version 0.999.0. À ce stade, vous remarqueriez également un changement dans votre fichier Cargo.lock indiquant que la version de la crate rand que vous utilisez maintenant est 0.8.6. Pour utiliser la version 0.999.0 de rand ou toute version de la série 0.999.x, vous devriez mettre à jour le fichier Cargo.toml pour qu’il ressemble à ceci (ne faites pas réellement cette modification car les exemples suivants supposent que vous utilisez rand 0.8) :

[dependencies]
rand = "0.999.0"

La prochaine fois que vous exécuterez cargo build, Cargo mettra à jour le registre des crates disponibles et réévaluera vos exigences pour rand selon la nouvelle version que vous avez spécifiée.

Il y a beaucoup plus à dire sur Cargo et son écosystème, ce que nous aborderons au chapitre 14, mais pour l’instant, c’est tout ce que vous devez savoir. Cargo facilite la réutilisation des bibliothèques, les Rustaceans peuvent donc écrire des projets plus petits assemblés à partir d’un certain nombre de paquets.

Générer un nombre aléatoire

Commençons à utiliser rand pour générer un nombre à deviner. L’étape suivante est de mettre à jour src/main.rs, comme montré dans l’encart 2-3.

Filename: src/main.rs
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}
Listing 2-3: Adding code to generate a random number

D’abord, nous ajoutons la ligne use rand::Rng;. Le trait Rng définit des méthodes que les générateurs de nombres aléatoires implémentent, et ce trait doit être dans la portée pour que nous puissions utiliser ces méthodes. Le chapitre 10 couvrira les traits en détail.

Ensuite, nous ajoutons deux lignes au milieu. Dans la première ligne, nous appelons la fonction rand::thread_rng qui nous donne le générateur de nombres aléatoires particulier que nous allons utiliser : un générateur local au thread d’exécution actuel et initialisé par le système d’exploitation. Puis, nous appelons la méthode gen_range sur le générateur de nombres aléatoires. Cette méthode est définie par le trait Rng que nous avons importé dans la portée avec l’instruction use rand::Rng;. La méthode gen_range prend une expression d’intervalle comme argument et génère un nombre aléatoire dans cet intervalle. Le type d’expression d’intervalle que nous utilisons ici prend la forme start..=end et est inclusif sur les bornes inférieure et supérieure, nous devons donc spécifier 1..=100 pour demander un nombre entre 1 et 100.

Remarque : vous ne saurez pas spontanément quels traits utiliser et quelles méthodes et fonctions appeler depuis une crate, c’est pourquoi chaque crate dispose d’une documentation avec des instructions d’utilisation. Une autre fonctionnalité pratique de Cargo est que l’exécution de la commande cargo doc --open construira la documentation fournie par toutes vos dépendances localement et l’ouvrira dans votre navigateur. Si vous êtes intéressé par d’autres fonctionnalités de la crate rand, par exemple, exécutez cargo doc --open et cliquez sur rand dans la barre latérale de gauche.

La deuxième nouvelle ligne affiche le nombre secret. C’est utile pendant que nous développons le programme pour pouvoir le tester, mais nous la supprimerons de la version finale. Ce n’est pas vraiment un jeu si le programme affiche la réponse dès qu’il démarre !

Essayez d’exécuter le programme quelques fois :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

Vous devriez obtenir des nombres aléatoires différents, et ils devraient tous être des nombres entre 1 et 100. Bon travail !

Comparer la proposition au nombre secret

Maintenant que nous avons la saisie de l’utilisateur et un nombre aléatoire, nous pouvons les comparer. Cette étape est montrée dans l’encart 2-4. Notez que ce code ne compilera pas encore, comme nous allons l’expliquer.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    // --snip--
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
Listing 2-4: Handling the possible return values of comparing two numbers

D’abord, nous ajoutons une autre instruction use, important un type appelé std::cmp::Ordering depuis la bibliothèque standard. Le type Ordering est un autre enum et possède les variantes Less, Greater et Equal. Ce sont les trois résultats possibles lorsque vous comparez deux valeurs.

Ensuite, nous ajoutons cinq nouvelles lignes en bas qui utilisent le type Ordering. La méthode cmp compare deux valeurs et peut être appelée sur tout ce qui peut être comparé. Elle prend une référence vers ce avec quoi vous voulez comparer : ici, elle compare guess à secret_number. Puis, elle renvoie une variante de l’enum Ordering que nous avons importé dans la portée avec l’instruction use. Nous utilisons une expression [match][match] pour décider quoi faire ensuite en fonction de la variante d’Ordering renvoyée par l’appel à cmp avec les valeurs de guess et secret_number.

Une expression match est composée de branches. Une branche consiste en un motif auquel correspondre et le code qui devrait être exécuté si la valeur donnée au match correspond au motif de cette branche. Rust prend la valeur donnée au match et examine le motif de chaque branche tour à tour. Les motifs et la construction match sont des fonctionnalités puissantes de Rust : ils vous permettent d’exprimer une variété de situations que votre code pourrait rencontrer et s’assurent que vous les gérez toutes. Ces fonctionnalités seront couvertes en détail aux chapitres 6 et 19, respectivement.

Parcourons un exemple avec l’expression match que nous utilisons ici. Disons que l’utilisateur a proposé 50 et que le nombre secret généré aléatoirement cette fois est 38.

Lorsque le code compare 50 à 38, la méthode cmp renverra Ordering::Greater car 50 est supérieur à 38. L’expression match reçoit la valeur Ordering::Greater et commence à vérifier le motif de chaque branche. Elle regarde le motif de la première branche, Ordering::Less, et constate que la valeur Ordering::Greater ne correspond pas à Ordering::Less, donc elle ignore le code de cette branche et passe à la suivante. Le motif de la branche suivante est Ordering::Greater, qui correspond bien à Ordering::Greater ! Le code associé à cette branche s’exécutera et affichera Too big! à l’écran. L’expression match se terminé après la première correspondance réussie, donc elle ne regardera pas la dernière branche dans ce scénario.

Cependant, le code de l’encart 2-4 ne compilera pas encore. Essayons :

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                 --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
   |                 |
   |                 arguments to this method are incorrect
   |
   = note: expected reference `&String`
              found reference `&{integer}`
note: method defined here
  --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error

Le coeur de l’erreur indique qu’il y à des types incompatibles. Rust possède un système de types fort et statique. Cependant, il dispose également de l’inférence de types. Lorsque nous avons écrit let mut guess = String::new(), Rust a pu inférer que guess devait être un String et ne nous a pas demandé d’écrire le type. Le secret_number, en revanche, est un type numérique. Plusieurs types numériques de Rust peuvent avoir une valeur entre 1 et 100 : i32, un nombre 32 bits ; u32, un nombre 32 bits non signé ; i64, un nombre 64 bits ; ainsi que d’autres. Sauf indication contraire, Rust utilise par défaut un i32, qui est le type de secret_number à moins que vous n’ajoutiez des informations de type ailleurs qui amèneraient Rust à inférer un type numérique différent. La raison de l’erreur est que Rust ne peut pas comparer une chaîne de caractères et un type numérique.

En fin de compte, nous voulons convertir le String que le programme lit en entrée en un type numérique afin de pouvoir le comparer numériquement au nombre secret. Nous le faisons en ajoutant cette ligne au corps de la fonction main : Fichier : src/main.rs rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-03-convert-string-to-number/src/main.rs:here}}

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    // --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

La ligne est :

let guess: u32 = guess.trim().parse().expect("Please type a number!");

Nous créons une variable nommée guess. Mais attendez, le programme n’a-t-il pas déjà une variable nommée guess ? C’est le cas, mais Rust nous permet utilement de masquer la valeur précédente de guess avec une nouvelle. Le masquage (shadowing) nous permet de réutiliser le nom de variable guess plutôt que de nous forcer à créer deux variables distinctes, comme guess_str et guess, par exemple. Nous couvrirons cela plus en détail au [chapitre 3][shadowing], mais pour l’instant, sachez que cette fonctionnalité est souvent utilisée lorsque vous voulez convertir une valeur d’un type vers un autre type.

Nous lions cette nouvelle variable à l’expression guess.trim().parse(). Le guess dans l’expression fait référence à la variable guess originale qui contenait la saisie sous forme de chaîne. La méthode trim sur une instance de String éliminera tous les espaces blancs au début et à la fin, ce que nous devons faire avant de pouvoir convertir la chaîne en u32, qui ne peut contenir que des données numériques. L’utilisateur doit appuyer sur Entrée pour valider read_line et saisir sa proposition, ce qui ajouté un caractère de nouvelle ligne à la chaîne. Par exemple, si l’utilisateur tape 5 et appuie sur Entrée, guess ressemble à ceci : 5 . Le représente un “retour à la ligne”. (Sous Windows, appuyer sur Entrée produit un retour chariot et un retour à la ligne, \r .) La méthode trim élimine ou \r , ne laissant que 5.

La méthode parse sur les chaînes convertit une chaîne vers un autre type. Ici, nous l’utilisons pour convertir une chaîne en nombre. Nous devons indiquer à Rust le type numérique exact que nous voulons en utilisant let guess: u32. Les deux-points (:) après guess indiquent à Rust que nous annoterons le type de la variable. Rust possède quelques types numériques intégrés ; le u32 vu ici est un entier non signé de 32 bits. C’est un bon choix par défaut pour un petit nombre positif. Vous en apprendrez davantage sur les autres types numériques au chapitre 3.

De plus, l’annotation u32 dans ce programme d’exemple et la comparaison avec secret_number signifient que Rust inférera que secret_number devrait également être un u32. Ainsi, la comparaison se fera maintenant entre deux valeurs du même type !

La méthode parse ne fonctionnera que sur des caractères qui peuvent logiquement être convertis en nombres et peut donc facilement provoquer des erreurs. Si, par exemple, la chaîne contenait A👍%, il n’y aurait aucun moyen de la convertir en nombre. Comme elle peut échouer, la méthode parse renvoie un type Result, tout comme la méthode read_line (discutée précédemment dans “Gérer les erreurs potentielles avec Result). Nous traiterons ce Result de la même manière en utilisant à nouveau la méthode expect. Si parse renvoie une variante Err de Result parce qu’elle n’a pas pu créer un nombre à partir de la chaîne, l’appel à expect fera planter le jeu et affichera le message que nous lui donnons. Si parse peut convertir avec succès la chaîne en nombre, elle renverra la variante Ok de Result, et expect renverra le nombre que nous voulons à partir de la valeur Ok.

Exécutons le programme maintenant :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

Super ! Même si des espaces ont été ajoutés avant la proposition, le programme a quand même déterminé que l’utilisateur a proposé 76. Exécutez le programme plusieurs fois pour vérifier le comportement différent avec différents types de saisie : devinez le nombre correctement, proposez un nombre trop élevé et proposez un nombre trop bas.

La majeure partie du jeu fonctionne maintenant, mais l’utilisateur ne peut faire qu’une seule proposition. Changeons cela en ajoutant une boucle !

Permettre plusieurs propositions avec une boucle

Le mot-clé loop crée une boucle infinie. Nous allons ajouter une boucle pour donner aux utilisateurs plus de chances de deviner le nombre : Fichier : src/main.rs rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-04-looping/src/main.rs:here}}

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    // --snip--

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        // --snip--


        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

Comme vous pouvez le voir, nous avons déplacé tout ce qui suit l’invite de saisie dans une boucle. Assurez-vous d’indenter les lignes à l’intérieur de la boucle de quatre espaces supplémentaires chacune et relancez le programme. Le programme demandera désormais une nouvelle proposition indéfiniment, ce qui introduit en fait un nouveau problème. Il semble que l’utilisateur ne puisse pas quitter !

L’utilisateur pourrait toujours interrompre le programme en utilisant le raccourci clavier ctrl-C. Mais il existe une autre façon d’échapper à ce monstre insatiable, comme mentionné dans la discussion sur parse dans « Comparer la supposition au nombre secret » : si le jeu obtient un nombre non valide, le programme plantera. Nous allons exploiter cela pour permettre à l’utilisateur de quitter en cas d’erreur, comme le montre l’Encart 2-5 :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit

thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Taper quit quittera le jeu, mais comme vous le remarquerez, saisir toute autre entrée non numérique le fera également. C’est sous-optimal, c’est le moins que l’on puisse dire ; nous voulons que le jeu s’arrête également lorsque le bon nombre est deviné.

Quitter après une bonne réponse

Programmons le jeu pour qu’il se terminé lorsque l’utilisateur gagne en ajoutant une instruction break : Fichier : src/main.rs rust,ignore {{#rustdoc_include ../listings/ch02-guessing-game-tutorial/no-listing-05-quitting/src/main.rs:here}}

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse().expect("Please type a number!");

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Ajouter la ligne break après You win! fait sortir le programme de la boucle lorsque l’utilisateur devine correctement le nombre secret. Sortir de la boucle signifie également quitter le programme, car la boucle est la dernière partie de main.

Gérer les saisies invalides

Pour affiner davantage le comportement du jeu, plutôt que de faire planter le programme lorsque l’utilisateur saisit autre chose qu’un nombre, faisons en sorte que le jeu ignore les saisies non numériques pour que l’utilisateur puisse continuer à deviner. Nous pouvons le faire en modifiant la ligne où guess est converti d’un String en u32, comme montré dans l’encart 2-5.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-5: Ignoring a non-number guess and asking for another guess instead of crashing the program

Nous passons d’un appel à expect à une expression match pour passer d’un plantage en cas d’erreur à une gestion de l’erreur. Rappelez-vous que parse renvoie un type Result et que Result est un enum avec les variantes Ok et Err. Nous utilisons ici une expression match, comme nous l’avons fait avec le résultat Ordering de la méthode cmp.

Si parse parvient à transformer la chaîne en nombre, elle renverra une valeur Ok contenant le nombre résultant. Cette valeur Ok correspondra au motif de la première branche, et l’expression match renverra simplement la valeur num que parse a produite et placée dans la valeur Ok. Ce nombre se retrouvera exactement là où nous le voulons, dans la nouvelle variable guess que nous créons.

Si parse n’est pas en mesure de transformer la chaîne en nombre, elle renverra une valeur Err contenant plus d’informations sur l’erreur. La valeur Err ne correspond pas au motif Ok(num) de la première branche du match, mais elle correspond au motif Err(_) de la seconde branche. Le tiret bas, _, est une valeur attrape-tout ; dans cet exemple, nous disons que nous voulons correspondre à toutes les valeurs Err, peu importe les informations qu’elles contiennent. Ainsi, le programme exécutera le code de la seconde branche, continue, qui indique au programme de passer à l’itération suivante de la boucle loop et de demander une autre proposition. De fait, le programme ignore toutes les erreurs que parse pourrait rencontrer !

Maintenant, tout dans le programme devrait fonctionner comme prévu. Essayons :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

Parfait ! Avec un dernier petit ajustement, nous terminerons le jeu de devinettes. Rappelez-vous que le programme affiche encore le nombre secret. Cela fonctionnait bien pour les tests, mais cela gâche le jeu. Supprimons le println! qui affiche le nombre secret. L’encart 2-6 montre le code final.

Filename: src/main.rs
use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}
Listing 2-6: Complete guessing game code

À ce stade, vous avez construit avec succès le jeu de devinettes. Félicitations !

Résumé

Ce projet était une façon pratique de vous présenter de nombreux nouveaux concepts de Rust : let, match, les fonctions, l’utilisation de crates externes, et bien plus. Dans les prochains chapitres, vous en apprendrez davantage sur ces concepts. Le chapitre 3 couvre les concepts que la plupart des langages de programmation possèdent, comme les variables, les types de données et les fonctions, et montre comment les utiliser en Rust. Le chapitre 4 explore la possession (ownership), une fonctionnalité qui distingue Rust des autres langages. Le chapitre 5 traite des structures (structs) et de la syntaxe des méthodes, et le chapitre 6 explique le fonctionnement des enums.