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
ifcomme expression et choisir entreloop,whileetfor.
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 :
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");
}
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 :
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 :
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");
}
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 :
| Type | Contenu | Exemple |
|---|---|---|
i8, i16, i32, i64, i128 | Entier signé (positif ou négatif) sur 8 à 128 bits | let annee: i32 = 2023; |
u8, u16, u32, u64, u128 | Entier non signé (jamais négatif) | let prix_ht: u32 = 89_900; |
usize, isize | Entier de la taille d'une adresse mémoire (64 bits sur un PC actuel) | Tailles et positions dans un tableau |
f32, f64 | Nombre à virgule flottante | let taux = 0.2; (f64 par défaut) |
bool | true ou false | let en_stock = false; |
char | Un caractère Unicode, sur 4 octets | let 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 :
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);
}
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 :
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}");
}
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 :
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);
}
89900 44 Err(TryFromIntError(()))
Ok(42)
1302.50 1302
| Outil | Quand l'utiliser | Si 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érification | Renvoie une erreur (Err) |
x as u8 | Conversion brute, à éviter entre entiers | Tronque 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 :
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);
}
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.0lit 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 quatreu32». Sa taille fait partie du type et ne change jamais. Pour une liste qui grandit, on utilise unVec(chapitre 6). - Les indices commencent à 0. Lire hors du tableau, par exemple
prix_ht[i]avec unicalculé 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, commeprix_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 -> :
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);
}
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_prixne renvoie rien : elle n'a pas de->. Son type de retour est alors(), le type « unité », qui ne contient aucune information.format!fonctionne commeprintln!mais renvoie uneStringau lieu d'afficher.{:02}complète par des zéros sur deux chiffres : 5 centimes s'affichent05.&strest le type d'un texte que l'on reçoit sans le posséder ; le chapitre 3 explique la différence avecString.- Les commentaires
///sont des commentaires de documentation :cargo docles 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 » :
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 :
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));
}
}
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
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}");
}
Total : 321600 centimes pour 4 postes
PC-0001
PC-0002
PC-0003
3 portables achetés, reste 30300 centimes
Renouvellement en 2028
| Boucle | Usage |
|---|---|
for x in collection | Parcourir un tableau, un intervalle ou une collection : la plus courante |
while condition | Répéter tant qu'une condition est vraie, sans savoir combien de fois |
loop | Ré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.
// A
fn main() {
let stock = 12;
stock = stock - 1;
println!("{stock}");
}
// B
fn main() {
let stock = "12";
let stock: u32 = stock.parse().unwrap();
println!("{}", stock + 1);
}
// C
fn double(x: u32) -> u32 {
x * 2;
}
fn main() {
println!("{}", double(4));
}
// 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) :
stockest immuable. Écrislet mut stock = 12;. - B compile et affiche
13. Le secondletmasque la première variable par une nouvelle, de typeu32. - C ne compile pas (E0308) : le point-virgule fait de
x * 2une instruction, et la fonction renvoie()au lieu d'unu32. Retire le point-virgule :x * 2. Le programme affiche alors8. - D ne compile pas (E0308 et E0277,
cannot add u64 to u32) : on n'additionne pas unu32et unu64. Convertis le plus petit vers le plus grand :u64::from(a) + b, qui affiche30.
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
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));
}
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
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);
}
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
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é ?