Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 226 minIntermédiaire

Variables, types, fonctions et boucles en Rust

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • déclarer des variables immuables ou mutables, et masquer une variable ;
  • choisir le bon type entier et éviter un dépassement ;
  • écrire une fonction qui renvoie une valeur sans return ;
  • utiliser if comme expression et choisir entre loop, while et for.

4 exercices corrigés

Les bases de Rust ressemblent à celles des autres langages : des variables, des types, des fonctions, des conditions et des boucles. Trois différences changent pourtant ta façon d'écrire : les variables sont immuables par défaut, chaque valeur a un type précis que le compilateur vérifie, et presque tout est une expression qui produit une valeur. Ce chapitre les passe en revue avec les premiers calculs de l'outil parc.

Déclarer une variable avec let

Une variable se déclare avec let. Le compilateur devine son type à partir de la valeur :

rust
fn main() {
    let numero = "PC-0001";
    let mut nombre_postes = 7;
    println!("Premier poste : {numero}, parc de {nombre_postes} postes");

    nombre_postes += 1;
    println!("Après l'achat : {nombre_postes} postes");
}
text
Premier poste : PC-0001, parc de 7 postes
Après l'achat : 8 postes

Sans mut, une variable ne peut plus changer une fois qu'elle a reçu sa valeur. Retire le mut et le compilateur refuse :

text
error[E0384]: cannot assign twice to immutable variable `nombre_postes`
 --> src/main.rs:3:5
  |
2 |     let nombre_postes = 7;
  |         ------------- first assignment to `nombre_postes`
3 |     nombre_postes += 1;
  |     ^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut nombre_postes = 7;
  |         +++

C'est l'inverse de JavaScript, où let est modifiable et const ne l'est pas. En Rust, tu dois demander la mutabilité. L'intérêt : en lisant let, tu sais que la valeur ne bougera plus, et le compilateur te prévient si tu déclares mut une variable que tu ne modifies jamais.

Masquage (shadowing) et constantes

Tu peux déclarer une nouvelle variable du même nom avec un nouveau let. C'est le masquage (shadowing) : la nouvelle variable cache l'ancienne, et elle peut avoir un autre type. C'est très utilisé pour transformer une saisie :

rust
const TVA: u32 = 20;
const AGE_RENOUVELLEMENT: i32 = 5;

fn main() {
    // Le prix saisi arrive sous forme de texte...
    let prix = " 899 ";
    // ... on le masque par un nombre du même nom.
    let prix: u32 = prix.trim().parse().unwrap();
    let prix = prix * 100; // en centimes
    println!("{prix} centimes, TVA {TVA} %, renouvellement à {AGE_RENOUVELLEMENT} ans");
}
text
89900 centimes, TVA 20 %, renouvellement à 5 ans

trim() retire les espaces, parse() convertit le texte en nombre et unwrap() récupère le résultat en supposant que tout s'est bien passé ; le chapitre 5 montre comment faire mieux. Le masquage n'est pas une modification : chaque let crée une variable neuve, et aucune n'est mut.

Une constante se déclare avec const, en dehors des fonctions si tu veux, et son type est obligatoire. Sa valeur doit être connue à la compilation : const TVA: u32 = 20;. Utilise-la pour les valeurs fixes de ton métier, plutôt que de semer des 20 dans le code.

Les types de base

Rust a des types scalaires, qui contiennent une seule valeur :

TypeContenuExemple
i8, i16, i32, i64, i128Entier signé (positif ou négatif) sur 8 à 128 bitslet annee: i32 = 2023;
u8, u16, u32, u64, u128Entier non signé (jamais négatif)let prix_ht: u32 = 89_900;
usize, isizeEntier de la taille d'une adresse mémoire (64 bits sur un PC actuel)Tailles et positions dans un tableau
f32, f64Nombre à virgule flottantelet taux = 0.2; (f64 par défaut)
booltrue ou falselet en_stock = false;
charUn caractère Unicode, sur 4 octetslet initiale = 'É';

Un entier sans indication de type est un i32, un nombre à virgule un f64. Le tiret bas dans 89_900 sépare les milliers pour la lisibilité ; le compilateur l'ignore. Les bornes sont accessibles par MIN et MAX :

rust
fn main() {
    println!("u8 : {} à {}", u8::MIN, u8::MAX);
    println!("i32 : {} à {}", i32::MIN, i32::MAX);
    println!("u32 : {} à {}", u32::MIN, u32::MAX);
    println!("0.1 + 0.2 = {}", 0.1 + 0.2);
}
text
u8 : 0 à 255
i32 : -2147483648 à 2147483647
u32 : 0 à 4294967295
0.1 + 0.2 = 0.30000000000000004

La dernière ligne n'est pas un bug de Rust : c'est le fonctionnement de tous les nombres à virgule flottante, en JavaScript, en C# ou en Python aussi. C'est pourquoi l'outil parc stocke les prix en centimes, dans un entier : 899,00 € devient 89900, et aucun centime ne se perd dans les calculs.

Dépassement d'entier et panique

Que se passe-t-il si un u8, qui monte jusqu'à 255, reçoit 255 + 1 ? En mode développement (cargo run), le programme s'arrête avec un message : c'est une panique (panic). En mode --release, le calcul « fait le tour » et donne 0, sans prévenir. Rust fournit des méthodes pour choisir le comportement :

rust
fn main() {
    let saisie = "255"; // une valeur lue au clavier, par exemple
    let stock: u8 = saisie.parse().unwrap();

    println!("{:?}", stock.checked_add(1)); // None : dépassement détecté
    println!("{}", stock.saturating_add(1)); // 255 : bloqué au maximum
    println!("{}", stock.wrapping_add(1)); // 0 : repart de zéro, voulu

    let nouveau_stock = stock + 1; // panique en mode debug
    println!("{nouveau_stock}");
}
text
None
255
0
thread 'main' (13768) panicked at src/main.rs:9:25:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Le nombre entre parenthèses après 'main' change à chaque exécution. checked_add renvoie None quand le résultat ne tient pas, et Some(valeur) sinon : c'est un Option, que le chapitre 5 détaille. Le format {:?} affiche une valeur dans sa forme de débogage, utile pour ces types.

Convertir entre types

Rust ne convertit jamais un type en un autre sans que tu le demandes. Additionner un u32 et un u64 est une erreur. Trois outils :

rust
fn main() {
    let prix_ht: u32 = 89900;
    let grand: u64 = u64::from(prix_ht); // sans perte : toujours possible
    let quantite: i32 = 300;
    let tronque = quantite as u8; // 300 ne tient pas dans un u8
    let essai = u8::try_from(quantite); // conversion vérifiée
    println!("{grand} {tronque} {essai:?}");
    println!("{:?}", u8::try_from(42_i32));
    let moyenne = 7815.0 / 6.0;
    println!("{moyenne:.2} {}", moyenne as u32);
}
text
89900 44 Err(TryFromIntError(()))
Ok(42)
1302.50 1302
OutilQuand l'utiliserSi la valeur ne tient pas
u64::from(x)Vers un type plus grand (u32 → u64)Impossible : le compilateur refuse u8::from(un_u32)
u8::try_from(x)Vers un type plus petit, avec vérificationRenvoie une erreur (Err)
x as u8Conversion brute, à éviter entre entiersTronque sans prévenir : 300 devient 44

Pour un nombre à virgule, as u32 coupe la partie décimale : 1302,5 devient 1302. Le format {:.2} affiche deux décimales.

Tuples et tableaux

Un tuple regroupe quelques valeurs de types différents. Un tableau (array) contient un nombre fixe de valeurs du même type :

rust
fn plus_cher(prix: [u32; 4]) -> (usize, u32) {
    let mut position = 0;
    for i in 1..prix.len() {
        if prix[i] > prix[position] {
            position = i;
        }
    }
    (position, prix[position])
}

fn main() {
    let poste: (&str, u32, bool) = ("PC-0001", 89900, true);
    println!("{} coûte {} centimes, affecté : {}", poste.0, poste.1, poste.2);

    let (numero, prix, _) = poste; // déstructuration
    println!("{numero} {prix}");

    let prix_ht: [u32; 4] = [89900, 64900, 104900, 61900];
    let (position, maximum) = plus_cher(prix_ht);
    println!("Le plus cher est en position {position} : {maximum} centimes");
    println!("{:?}", prix_ht);
}
text
PC-0001 coûte 89900 centimes, affecté : true
PC-0001 89900
Le plus cher est en position 2 : 104900 centimes
[89900, 64900, 104900, 61900]
  • poste.0 lit le premier élément du tuple ; let (numero, prix, _) = poste; le déstructure en trois variables, et _ ignore la troisième.
  • Le type [u32; 4] signifie « tableau de quatre u32 ». Sa taille fait partie du type et ne change jamais. Pour une liste qui grandit, on utilise un Vec (chapitre 6).
  • Les indices commencent à 0. Lire hors du tableau, par exemple prix_ht[i] avec un i calculé qui vaut 10, arrête le programme par une panique (index out of bounds) plutôt que de lire la zone mémoire voisine : c'est précisément le dépassement de tampon que Rust empêche. Si l'indice est écrit en dur, comme prix_ht[10], le compilateur refuse même de compiler.

Les fonctions

Une fonction se déclare avec fn. Le type de chaque paramètre est obligatoire, ainsi que le type renvoyé, après -> :

rust
const TVA: u64 = 20;

/// Prix TTC en centimes, à partir d'un prix HT en centimes.
fn prix_ttc(prix_ht: u32) -> u64 {
    u64::from(prix_ht) * (100 + TVA) / 100
}

/// 107880 centimes → « 1078,80 € ».
fn formater_euros(centimes: u64) -> String {
    format!("{},{:02} €", centimes / 100, centimes % 100)
}

fn afficher_prix(numero: &str, prix_ht: u32) {
    let ttc = prix_ttc(prix_ht);
    println!("{numero} : {} HT, {} TTC", formater_euros(u64::from(prix_ht)), formater_euros(ttc));
}

fn main() {
    afficher_prix("PC-0001", 89900);
    afficher_prix("PC-0005", 349000);
}
text
PC-0001 : 899,00 € HT, 1078,80 € TTC
PC-0005 : 3490,00 € HT, 4188,00 € TTC

Observe prix_ttc : il n'y a pas de return. En Rust, la dernière expression d'un bloc, écrite sans point-virgule, est sa valeur. Le calcul u64::from(prix_ht) * (100 + TVA) / 100 est donc la valeur renvoyée. Le résultat est un u64 parce qu'un u32 multiplié par 120 pourrait dépasser la limite du u32.

D'autres détails :

  • afficher_prix ne renvoie rien : elle n'a pas de ->. Son type de retour est alors (), le type « unité », qui ne contient aucune information.
  • format! fonctionne comme println! mais renvoie une String au lieu d'afficher. {:02} complète par des zéros sur deux chiffres : 5 centimes s'affichent 05.
  • &str est le type d'un texte que l'on reçoit sans le posséder ; le chapitre 3 explique la différence avec String.
  • Les commentaires /// sont des commentaires de documentation : cargo doc les transforme en pages web, et rust-analyzer les affiche au survol.
  • return valeur; existe aussi, pour sortir avant la fin de la fonction.

Conditions : if est une expression

La condition d'un if s'écrit sans parenthèses, et elle doit être un bool. Contrairement à JavaScript ou à C, un nombre n'est jamais « vrai » ou « faux » :

text
error[E0308]: mismatched types
 --> src/main.rs:3:8
  |
3 |     if nombre_postes {
  |        ^^^^^^^^^^^^^ expected `bool`, found integer

Écris la comparaison en entier : if nombre_postes > 0. Et comme if est une expression, il peut produire une valeur :

rust
fn etat(annee_achat: i32, annee_courante: i32) -> &'static str {
    let age = annee_courante - annee_achat;
    if age >= 5 {
        "à renouveler"
    } else if age >= 4 {
        "à prévoir l'an prochain"
    } else {
        "récent"
    }
}

fn main() {
    let nombre_postes = 7;
    let message = if nombre_postes > 1 { "postes" } else { "poste" };
    println!("{nombre_postes} {message}");
    for annee in [2020, 2022, 2025] {
        println!("{annee} : {}", etat(annee, 2026));
    }
}
text
7 postes
2020 : à renouveler
2022 : à prévoir l'an prochain
2025 : récent

let message = if … { "postes" } else { "poste" }; remplace l'opérateur ternaire ? : des autres langages, qui n'existe pas en Rust. Les deux branches doivent avoir le même type. Le type &'static str désigne un texte écrit en dur dans le programme, qui existe pendant toute son exécution.

Les opérateurs sont classiques : ==, !=, <, <=, >, >= pour comparer, && (et), || (ou), ! (non) pour combiner.

Les boucles : loop, while et for

rust
fn main() {
    // for : parcourir un tableau
    let prix_ht: [u32; 4] = [89900, 64900, 104900, 61900];
    let mut total: u64 = 0;
    for prix in prix_ht {
        total += u64::from(prix);
    }
    println!("Total : {total} centimes pour {} postes", prix_ht.len());

    // for sur un intervalle : 1..=3 inclut 3, 1..3 s'arrête à 2
    for numero in 1..=3 {
        println!("PC-{numero:04}");
    }

    // while : tant que la condition est vraie
    let mut budget: u32 = 300000;
    let mut achats = 0;
    while budget >= 89900 {
        budget -= 89900;
        achats += 1;
    }
    println!("{achats} portables achetés, reste {budget} centimes");

    // loop : boucle infinie, quittée par break, qui peut rendre une valeur
    let mut annee = 2023;
    let annee_renouvellement = loop {
        annee += 1;
        if annee - 2023 >= 5 {
            break annee;
        }
    };
    println!("Renouvellement en {annee_renouvellement}");
}
text
Total : 321600 centimes pour 4 postes
PC-0001
PC-0002
PC-0003
3 portables achetés, reste 30300 centimes
Renouvellement en 2028
BoucleUsage
for x in collectionParcourir un tableau, un intervalle ou une collection : la plus courante
while conditionRépéter tant qu'une condition est vraie, sans savoir combien de fois
loopRépéter sans fin, jusqu'à un break ; break valeur fait de la boucle une expression

continue passe au tour suivant, break sort de la boucle. Il n'existe pas de boucle for (i = 0; i < n; i++) comme en C ou en JavaScript : on écrit for i in 0..n. {numero:04} affiche le nombre sur quatre chiffres complétés par des zéros.

Exercices

Exercice 1 sur 4 Ce code compile-t-il ? Réussi À revoir

Pour chaque programme, dis s'il compile. S'il ne compile pas, explique pourquoi et corrige-le.

rust
// A
fn main() {
    let stock = 12;
    stock = stock - 1;
    println!("{stock}");
}
rust
// B
fn main() {
    let stock = "12";
    let stock: u32 = stock.parse().unwrap();
    println!("{}", stock + 1);
}
rust
// C
fn double(x: u32) -> u32 {
    x * 2;
}
fn main() {
    println!("{}", double(4));
}
rust
// D
fn main() {
    let a: u32 = 10;
    let b: u64 = 20;
    println!("{}", a + b);
}
Voir la réponseMasquer la réponse de l'exercice 1
  • A ne compile pas (E0384) : stock est immuable. Écris let mut stock = 12;.
  • B compile et affiche 13. Le second let masque la première variable par une nouvelle, de type u32.
  • C ne compile pas (E0308) : le point-virgule fait de x * 2 une instruction, et la fonction renvoie () au lieu d'un u32. Retire le point-virgule : x * 2. Le programme affiche alors 8.
  • D ne compile pas (E0308 et E0277, cannot add u64 to u32) : on n'additionne pas un u32 et un u64. Convertis le plus petit vers le plus grand : u64::from(a) + b, qui affiche 30.

Tu avais trouvé ?

Exercice 2 sur 4 Calculer une remise Réussi À revoir

Écris une fonction prix_remise(prix_ht: u32, pourcentage: u32) -> u32 qui applique une remise en pour cent à un prix en centimes. Teste-la dans main sur 89 900 centimes avec 10 % puis 15 %. Que se passe-t-il, en mode développement, si l'on appelle prix_remise(89900, 120) ?

Voir la réponseMasquer la réponse de l'exercice 2
rust
fn prix_remise(prix_ht: u32, pourcentage: u32) -> u32 {
    prix_ht * (100 - pourcentage) / 100
}

fn main() {
    println!("{}", prix_remise(89900, 10));
    println!("{}", prix_remise(89900, 15));
}
text
80910
76415

Avec 120 %, 100 - pourcentage vaut −20, qui ne tient pas dans un u32. En mode développement, le programme panique avec attempt to subtract with overflow. Une version robuste refuse ce cas, par exemple en limitant la remise : let pourcentage = pourcentage.min(100); en première ligne.

Tu avais trouvé ?

Exercice 3 sur 4 Combien de postes avec ce budget ? Réussi À revoir

Le service informatique dispose de 500 000 centimes (5 000 €). Un portable coûte 89 900 centimes HT. Écris une boucle while qui compte combien de portables on peut acheter et affiche le reste du budget. Puis obtiens le même résultat sans boucle, avec / et %.

Voir la réponseMasquer la réponse de l'exercice 3
rust
fn main() {
    let prix: u32 = 89900;
    let mut budget: u32 = 500000;
    let mut achats = 0;
    while budget >= prix {
        budget -= prix;
        achats += 1;
    }
    println!("{achats} portables, reste {budget} centimes");

    let total: u32 = 500000;
    println!("{} portables, reste {} centimes", total / prix, total % prix);
}
text
5 portables, reste 50500 centimes
5 portables, reste 50500 centimes

La division entière / donne le nombre de portables (5), le reste % ce qui reste du budget (500 000 − 5 × 89 900 = 50 500).

Tu avais trouvé ?

Exercice 4 sur 4 Afficher un tableau de numéros Réussi À revoir

Avec une boucle for sur un intervalle, affiche les numéros d'inventaire PC-0008 à PC-0012, chacun suivi de « (à étiqueter) ». Combien de lignes le programme affiche-t-il ?

Voir la réponseMasquer la réponse de l'exercice 4
rust
fn main() {
    for numero in 8..=12 {
        println!("PC-{numero:04} (à étiqueter)");
    }
}

Le programme affiche cinq lignes, de PC-0008 (à étiqueter) à PC-0012 (à étiqueter). Avec 8..12, sans le =, il s'arrêterait à PC-0011.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Ownership, emprunts et durées de vie : la mémoire en RustRevoir le sommaire du cours