Chapitre 02
Variables, types & mutabilité
Objectifs du chapitre
- Comprendre l'immutabilité par défaut de
letet savoir quand utilisermut - Distinguer
const,staticet le shadowing - Connaître les types scalaires de Rust et choisir entre
f32etf64pour du calcul robotique - Maîtriser les conversions explicites avec
aset les méthodes sûres de gestion de débordement - Manipuler des tuples pour représenter une pose robot
(x, y, theta)
1. let : immutable par défaut
Première surprise pour qui vient du C/C++ : en Rust, une variable déclarée avec let
est immuable par défaut. C'est l'inverse de l'habitude C/C++, où il faut ajouter
const explicitement pour interdire la réaffectation.
fn main() {
let vitesse_max = 2.5; // m/s — immuable
println!("Vitesse max : {vitesse_max}");
// vitesse_max = 3.0; // ERREUR de compilation : cannot assign twice
// // to immutable variable `vitesse_max`
}
Pour autoriser la réaffectation, il faut le déclarer explicitement avec mut :
fn main() {
let mut vitesse = 0.0_f64; // m/s
println!("Vitesse initiale : {vitesse}");
vitesse += 0.5; // accélération, autorisé grâce à `mut`
println!("Vitesse après accélération : {vitesse}");
}
Piège C/C++ → Rust : en C/C++, tout est mutable sauf mention contraire
(const). En Rust, c'est l'inverse : tout est const par défaut, et il faut
mut pour ouvrir la porte à la mutation. Ce choix n'est pas cosmétique : le compilateur
s'en sert pour prouver qu'aucune donnée n'est modifiée par erreur pendant qu'un autre bout de code
s'attend à ce qu'elle reste stable — un vrai atout en boucle de contrôle temps réel, où une variable
d'état modifiée « par accident » peut faire diverger un asservissement.
2. const et static
let (même immuable) est une liaison locale, évaluée à l'exécution. Pour des valeurs
vraiment constantes, connues à la compilation, Rust propose const et static :
// `const` : inlinée partout où elle est utilisée, pas d'adresse mémoire fixe.
// Le type est TOUJOURS obligatoire.
const PERIODE_BOUCLE_MS: u32 = 10; // boucle de contrôle à 100 Hz
// `static` : une seule instance en mémoire, à une adresse fixe, durée de vie 'static.
static NOM_ROBOT: &str = "R2-Atelier";
fn main() {
println!("{NOM_ROBOT} tourne à {PERIODE_BOUCLE_MS} ms par cycle");
}
En pratique, en robotique embarquée : const pour les constantes numériques
(périodes, limites physiques, gains par défaut), static surtout pour des données
partagées globalement (rare et à éviter si possible — préférez passer l'état explicitement).
Une static mut existe mais son accès est unsafe ; on la reverra au
chapitre sur le temps réel.
3. Le shadowing
Le shadowing consiste à redéclarer une nouvelle variable avec le même nom via let.
Ce n'est pas de la mutation : chaque let x = ... crée une variable
distincte qui masque la précédente. On peut même changer de type au passage.
fn main() {
let angle = "45"; // &str, lu depuis une config par exemple
let angle: f64 = angle.parse().expect("angle invalide");
let angle = angle.to_radians(); // même nom, nouvelle variable, type inchangé ici
println!("Angle en radians : {angle}");
}
Shadowing vs mut : avec mut, c'est la même variable,
même type, dont la valeur change en place. Avec le shadowing, ce sont des variables différentes
(potentiellement de types différents), et l'ancienne reste immuable — elle est juste devenue
inaccessible par ce nom. Le shadowing est idéal pour des transformations successives
(parsing → conversion → validation) sans polluer l'espace de noms avec angle_str,
angle_deg, angle_rad.
4. Types scalaires
Rust a un système de types statique et explicite. Les scalaires de base :
- Entiers signés :
i8,i16,i32,i64,i128,isize - Entiers non signés :
u8,u16,u32,u64,u128,usize - Flottants :
f32,f64 - Booléen :
bool(true/false, jamais0/1implicite) - Caractère :
char, un scalaire Unicode sur 4 octets (pas juste ASCII comme lecharC)
isize et usize ont la taille d'un pointeur sur la plateforme cible
(souvent 64 bits) — l'équivalent de ssize_t/size_t en C. On les utilise
surtout pour indexer des collections, pas pour du calcul physique.
fn main() {
let compteur_cycles: u64 = 0;
let erreur_position: i32 = -3; // en pas moteur, peut être négatif
let asservissement_actif: bool = true;
let unite: char = 'm'; // mètres
println!("{compteur_cycles} {erreur_position} {asservissement_actif} {unite}");
}
5. f32 vs f64 pour le calcul robotique
Rust infère f64 par défaut pour un littéral flottant sans annotation — contrairement
au C où un littéral comme 1.0 est un double aussi, mais où l'on bascule
souvent vers float pour la performance sans trop y penser. En Rust, ce choix doit être
délibéré :
| Critère | f32 | f64 |
|---|---|---|
| Précision | ~7 chiffres significatifs | ~15-17 chiffres significatifs |
| Mémoire | 4 octets | 8 octets |
| Débit SIMD / cache | 2× plus de valeurs par registre | moitié moins |
| Usage typique | GPU, gros volumes (nuages de points, images), microcontrôleurs sans FPU double | calculs numériques accumulés (intégration, filtre de Kalman, cinématique) |
Recommandation pour ce cours : on utilisera f64 par défaut pour
toute la partie « algèbre / cinématique / filtrage », car les erreurs d'arrondi s'accumulent vite
dans une boucle de contrôle qui tourne à 100 Hz ou 1 kHz pendant des minutes. On ne descend en
f32 que si le profilage montre un vrai goulot mémoire/CPU (typiquement sur cible
embarquée sans FPU 64 bits, ou traitement massif de nuages de points 3D).
6. Inférence et annotations explicites
Rust infère le type dans la grande majorité des cas, mais accepte (et recommande parfois) une annotation explicite pour la lisibilité ou quand l'inférence est ambiguë :
fn main() {
let pas = 0.01; // inféré : f64
let angle_cible: f64 = 1.57; // annotation explicite, utile pour la doc du code
let nb_articulations = 6u8; // suffixe de type directement sur le littéral
println!("{pas} {angle_cible} {nb_articulations}");
}
7. Débordement d'entiers
En C, un débordement d'entier signé est un comportement indéfini, et un débordement d'entier non signé « boucle » silencieusement (wrapping). Rust est plus strict et plus prévisible :
- En profil debug, un débordement déclenche un
panic!à l'exécution — l'erreur est détectée immédiatement. - En profil release (optimisé), par défaut, le débordement fait un wrapping silencieux (comportement identique au C pour les non signés), pour ne pas payer le coût de la vérification en production.
fn main() {
let compteur_pas: u8 = 250;
// let depassement = compteur_pas + 10; // panic en debug ! (u8 max = 255)
// Méthodes explicites et sûres, indépendantes du profil de compilation :
let a = compteur_pas.wrapping_add(10); // boucle silencieusement : 4
let b = compteur_pas.checked_add(10); // None si débordement, Some(x) sinon
let c = compteur_pas.saturating_add(10); // bloque à la valeur max : 255
println!("wrapping={a} checked={b:?} saturating={c}");
}
Boucle temps réel : ne comptez jamais sur le comportement « debug panique,
release enroule » pour la logique métier — c'est fragile et dépend du profil de build. Dans du
code embarqué, préférez systématiquement les méthodes explicites : checked_* quand
une erreur doit être remontée proprement (avec Option/Result, sans
branche imprévisible ni panic en vol), saturating_* pour des compteurs ou limites
physiques qui doivent simplement plafonner (ex. une commande PWM qui ne doit jamais dépasser sa
valeur max), et wrapping_* seulement quand le rebouclage est le comportement voulu
(ex. un compteur de cycles circulaire).
8. Conversions avec as
Rust n'effectue aucune conversion implicite entre types numériques, même entre
i32 et i64, ou f32 et f64. Il faut convertir
explicitement avec as (ou des méthodes comme TryFrom pour des conversions
pouvant échouer).
fn main() {
let pas_moteur: i32 = 4096;
let pas_moteur_f: f64 = pas_moteur as f64; // conversion explicite, sans perte ici
let angle_rad: f64 = 3.14159265358979;
let angle_f32: f32 = angle_rad as f32; // perte de précision assumée et visible
let grand: i64 = 1_000_000_000_000;
let tronque: i32 = grand as i32; // troncature silencieuse : le résultat n'est PAS 1e12 !
println!("{pas_moteur_f} {angle_f32} {tronque}");
}
Piège C/C++ → Rust : en C, écrire double d = mon_int; ou
float f = mon_double; compile sans broncher, avec parfois une perte de précision
silencieuse (voire un avertissement ignoré). En Rust, l'absence de conversion implicite force à
écrire as f64 ou as f32 à chaque endroit où une perte de précision ou
une troncature peut se produire : c'est plus verbeux, mais chaque conversion est un point visible
et grep-able dans le code, ce qui aide énormément à traquer les bugs numériques dans un pipeline
de calcul (odométrie, filtrage, commande).
9. Tuples et déstructuration
Un tuple regroupe plusieurs valeurs de types potentiellement différents, sans avoir besoin de définir une structure nommée. Pratique pour retourner plusieurs valeurs d'une fonction, ou pour une donnée légère et locale comme une pose 2D.
fn main() {
// Une pose robot 2D : position (x, y) en mètres, orientation theta en radians
let pose: (f64, f64, f64) = (1.20, 0.35, 0.78);
// Accès par index
println!("x = {}", pose.0);
// Déstructuration : la manière idiomatique de lire un tuple
let (x, y, theta) = pose;
println!("x={x} y={y} theta={theta}");
}
On peut ignorer certains champs avec _ : let (x, y, _) = pose; si
l'orientation ne nous intéresse pas ici. Utile pour garder le code expressif sans variables
inutilisées (le compilateur avertirait sinon).
10. Fil rouge : angles en radians
Dans ce cours, tous les calculs d'angle internes se font en radians, en
f64, conformément à la convention de la plupart des bibliothèques scientifiques
(et des fonctions trigonométriques de std). On ne convertit en degrés qu'à l'affichage
ou à la saisie utilisateur.
use std::f64::consts::PI;
fn deg_vers_rad(deg: f64) -> f64 {
deg * PI / 180.0
}
fn rad_vers_deg(rad: f64) -> f64 {
rad * 180.0 / PI
}
fn main() {
let angle_articulation_deg = 90.0;
let angle_rad = deg_vers_rad(angle_articulation_deg);
println!("{angle_articulation_deg}° = {angle_rad} rad");
println!("retour : {} °", rad_vers_deg(angle_rad));
}
Rust fournit aussi les méthodes f64::to_radians() et f64::to_degrees()
directement sur les flottants — pas besoin de réécrire ces fonctions dans du vrai code :
90.0_f64.to_radians(). On les a réécrites ici pour illustrer une fonction simple ;
dans la suite du cours on utilisera les méthodes natives.
Exercices
Exercice 1 — Convertir des degrés en radians
Écris un programme qui déclare un angle en degrés (f64), le convertit en radians
avec to_radians(), puis affiche les deux valeurs avec 4 décimales
(indice : {valeur:.4} dans une macro println!).
Voir la solution
fn main() {
let angle_deg: f64 = 135.0;
let angle_rad = angle_deg.to_radians();
println!("{angle_deg:.4}° = {angle_rad:.4} rad");
}
to_radians() est une méthode fournie par f64 (et f32),
équivalente à angle_deg * PI / 180.0. Le format {valeur:.4} dans
println! arrondit l'affichage à 4 décimales sans changer la valeur réelle stockée.
Exercice 2 — Expérimenter le shadowing
Pars d'une chaîne "3.14", transforme-la successivement en f64, puis
arrondis-la à l'entier le plus proche (f64::round()) en réutilisant à chaque fois le
même nom de variable valeur grâce au shadowing. Affiche le résultat final et son type
réel en commentaire.
Voir la solution
fn main() {
let valeur = "3.14"; // &str
let valeur: f64 = valeur.parse().unwrap(); // f64
let valeur = valeur.round(); // toujours f64, mais arrondi : 3.0
println!("{valeur}");
}
Trois variables valeur distinctes coexistent dans la portée, chacune masquant la
précédente. C'est différent de let mut valeur = ...; valeur = ...;, qui aurait
exigé que le type reste identique (f64 ne peut pas être réaffecté à partir d'un
&str).
Exercice 3 — Tester un débordement
Déclare un u8 à sa valeur maximale (u8::MAX). Calcule
valeur.checked_add(1), valeur.wrapping_add(1) et
valeur.saturating_add(1), et affiche les trois résultats. Explique en commentaire
pourquoi les trois diffèrent.
Voir la solution
fn main() {
let valeur: u8 = u8::MAX; // 255
let a = valeur.checked_add(1); // None : 256 ne rentre pas dans un u8
let b = valeur.wrapping_add(1); // 0 : rebouclage silencieux (255 + 1 mod 256)
let c = valeur.saturating_add(1); // 255 : plafonné à la valeur max du type
println!("checked={a:?} wrapping={b} saturating={c}");
}
checked_add retourne un Option<u8> : None signale
explicitement l'échec, à traiter par l'appelant. wrapping_add reproduit le
comportement arithmétique modulaire du C pour les entiers non signés. saturating_add
bloque à la borne du type — souvent le comportement le plus sûr pour une commande physique
(ex. un rapport cyclique PWM qui ne doit jamais « déborder » vers une valeur aberrante).
Exercice 4 — Déstructurer une pose robot
Déclare un tuple pose: (f64, f64, f64) représentant (x, y, theta) d'un
robot mobile, avec theta en degrés (par exemple 45.0). Déstructure-le en trois
variables nommées, convertis theta en radians dans la foulée (par shadowing), puis
affiche une phrase récapitulative avec les trois valeurs.
Voir la solution
fn main() {
let pose: (f64, f64, f64) = (2.0, -0.5, 45.0); // x (m), y (m), theta (deg)
let (x, y, theta) = pose;
let theta = theta.to_radians(); // shadowing : theta devient l'angle en radians
println!("Robot en ({x}, {y}) avec un cap de {theta:.4} rad");
}
La déstructuration let (x, y, theta) = pose; extrait les trois champs en une seule
ligne, plus lisible que trois accès pose.0, pose.1, pose.2.
Le shadowing sur theta permet de passer de « degrés lus quelque part » à « radians
utilisés en interne » sans introduire de nouveau nom.
Récapitulatif
letest immuable par défaut ;let mutautorise la mutation en place.const(typée, inlinée) etstatic(une instance, adresse fixe) servent pour des valeurs connues à la compilation.- Le shadowing crée une nouvelle variable (type libre) ; ce n'est pas une mutation de la variable existante.
- Types scalaires : entiers dimensionnés (
i8…i128,u8…u128,isize/usize),f32/f64,bool,char. - Pour du calcul robotique accumulé (cinématique, filtrage), préférer
f64; réserverf32aux cas où mémoire/débit priment sur la précision. - Le débordement panique en debug, s'enroule en release par défaut : en code temps réel, préférer explicitement
checked_*,saturating_*ouwrapping_*. - Aucune conversion numérique implicite : toute conversion passe par
as(ouTryFrom), ce qui rend les pertes de précision visibles dans le code. - Les tuples et leur déstructuration
let (x, y, theta) = pose;sont pratiques pour des données groupées légères comme une pose robot. - Fil rouge : angles internes toujours en radians,
f64, conversions viato_radians()/to_degrees()oustd::f64::consts::PI.