Chapitre 03
Fonctions & contrôle de flux
Objectifs du chapitre
- Déclarer des fonctions typées et comprendre la différence entre expression et instruction.
- Utiliser
if/elseetmatchcomme expressions produisant une valeur. - Maîtriser les trois formes de boucles (
loop,while,for) et les étiquettes de boucle. - Écrire des fonctions de conversion et de classification typiques d'un pipeline de contrôle robotique.
- Identifier les constructions de contrôle de flux adaptées au temps réel (bornées, déterministes).
1. Déclarer une fonction
En Rust, une fonction se déclare avec le mot-clé fn. Chaque paramètre doit avoir un type
explicite (pas d'inférence sur les paramètres, contrairement aux variables locales), et le type de retour
s'annonce après une flèche ->. Si la fonction ne retourne rien, on omet la flèche
(le type de retour est alors implicitement (), le « tuple unité »).
// Convertit une vitesse angulaire en tours par seconde.
fn rad_par_s_vers_tours_par_s(omega: f64) -> f64 {
omega / (2.0 * std::f64::consts::PI)
}
// Fonction sans valeur de retour : type () implicite.
fn journaliser(message: &str) {
println!("[LOG] {message}");
}
fn main() {
let vitesse = rad_par_s_vers_tours_par_s(31.4);
journaliser(&format!("vitesse = {vitesse:.3} tr/s"));
}
Contrairement au C, l'ordre de déclaration des fonctions au sein d'un module n'a pas d'importance :
main peut appeler une fonction définie plus bas dans le fichier sans prototype préalable.
2. Expressions vs instructions
C'est l'un des points qui surprend le plus les personnes venant du C/C++. En Rust, presque tout est une
expression qui produit une valeur : un bloc { ... }, un if, un
match. Une instruction (comme une affectation let x = 5;), elle,
ne produit rien et se termine par un point-virgule.
La règle clé : dans un bloc de fonction, si la dernière ligne n'a pas de point-virgule, sa valeur
devient la valeur de retour du bloc (donc, en fin de fonction, la valeur de retour de la fonction).
Ajouter un point-virgule par réflexe — comme en C où chaque ligne se termine ainsi — transforme
l'expression en instruction qui retourne (), ce qui casse la compilation si le type de retour
attendu n'est pas ().
fn carre(x: f64) -> f64 {
x * x // pas de point-virgule : c'est la valeur retournée
}
fn carre_bugue(x: f64) -> f64 {
x * x; // erreur de compilation : ceci retourne (), pas f64
}
Ajouter un ; à la dernière ligne d'une fonction est l'erreur numéro un des débutant·e·s
venant du C/C++. Le compilateur Rust affiche alors un message explicite du type
« expected `f64`, found `()`» — c'est un excellent réflexe de vérifier immédiatement
la dernière ligne du bloc quand cette erreur apparaît.
On peut bien sûr utiliser return pour un retour anticipé, typiquement dans une garde
en début de fonction — le mot-clé reste utile et parfaitement idiomatique dans ce cas :
fn inverse(x: f64) -> Option<f64> {
if x == 0.0 {
return None; // retour anticipé explicite
}
Some(1.0 / x)
}
3. if/else comme expression
Puisque if est une expression, on peut directement l'assigner à une variable — plus besoin
de l'opérateur ternaire ?: du C. Les deux branches doivent impérativement retourner le
même type.
fn saturer(valeur: f64, limite: f64) -> f64 {
let resultat = if valeur > limite {
limite
} else if valeur < -limite {
-limite
} else {
valeur
};
resultat
}
Comme les deux branches d'un if-expression doivent avoir le même type, le compilateur
détecte immédiatement les incohérences (par exemple oublier une branche else alors que
la valeur est utilisée) — une classe entière de bugs de logique de commande est éliminée à la compilation.
4. match : le switch qui ne laisse rien passer
match ressemble au switch du C, mais avec deux différences majeures. D'abord,
il est exhaustif : le compilateur refuse de compiler si un cas possible n'est pas couvert
(fini les oublis de default qui font planter le robot en silence). Ensuite, match
est lui aussi une expression qui peut produire une valeur, et il n'y a jamais de
fall-through implicite comme en C — pas besoin de break.
fn classer_code_erreur(code: i32) -> &'static str {
match code {
0 => "OK",
1 | 2 => "avertissement mineur", // plusieurs motifs
3..=9 => "erreur récupérable", // plage inclusive
10..=99 => "erreur critique",
_ => "code inconnu", // motif « fourre-tout » obligatoire
}
}
Le motif _ joue le rôle du default du C, mais ici il n'est requis que si les
autres motifs ne couvrent pas déjà toutes les valeurs possibles du type. Sur une énumération (enum),
il est souvent possible — et préférable — de couvrir explicitement chaque variante plutôt que de se
reposer sur _, afin que le compilateur signale toute variante oubliée si l'énumération
évolue plus tard.
enum EtatCapteur {
Ok,
HorsLimite,
Deconnecte,
}
fn classer_capteur(etat: &EtatCapteur) -> &'static str {
match etat {
EtatCapteur::Ok => "capteur nominal",
EtatCapteur::HorsLimite => "valeur hors plage — vérifier le montage",
EtatCapteur::Deconnecte => "capteur absent du bus",
// pas de `_` nécessaire : les trois variantes sont couvertes
}
}
En robotique, cette exhaustivité est précieuse : si on ajoute plus tard une variante
EtatCapteur::Saturation, chaque match sur cette énumération qui ne la gère pas
encore provoquera une erreur de compilation, pas un bug silencieux découvert en test
sur banc, voire pire, sur le robot en mouvement.
5. Les trois formes de boucles
Rust propose loop, while et for. Contrairement au C, il n'y a
pas de boucle for à trois clauses (for(i=0; i<n; i++)) : on itère sur un
itérateur, le plus souvent une plage (range).
// loop : boucle infinie explicite, sort avec break (éventuellement avec une valeur)
fn attendre_stabilisation(seuil: f64, mesures: &[f64]) -> usize {
let mut i = 0;
let indice_stable = loop {
if i >= mesures.len() || mesures[i].abs() < seuil {
break i; // `break` avec une valeur : loop devient une expression
}
i += 1;
};
indice_stable
}
// while : condition classique évaluée avant chaque itération
fn compter_jusqu_a_zero(mut compteur: i32) {
while compteur > 0 {
compteur -= 1;
}
}
// for : itération sur une plage — bornée par construction
fn somme_plage(n: u32) -> u32 {
let mut total = 0;
for i in 0..n { // 0..n : exclut n
total += i;
}
total
}
fn somme_plage_inclusive(n: u32) -> u32 {
let mut total = 0;
for i in 0..=n { // 0..=n : inclut n
total += i;
}
total
}
// for sur une collection : pas d'indices à gérer manuellement
fn moyenne(mesures: &[f64]) -> f64 {
let mut somme = 0.0;
for m in mesures {
somme += m;
}
somme / mesures.len() as f64
}
Les étiquettes de boucle (labels) permettent de cibler précisément quelle boucle
break ou continue affecte, très utile dans des boucles imbriquées de
balayage de capteurs ou de recherche dans une grille d'occupation :
fn chercher_collision(grille: &[[bool; 4]; 4]) -> Option<(usize, usize)> {
let mut trouve = None;
'lignes: for i in 0..4 {
for j in 0..4 {
if grille[i][j] {
trouve = Some((i, j));
break 'lignes; // sort directement de la boucle externe étiquetée
}
}
}
trouve
}
Dans un contexte temps réel, privilégiez systématiquement les boucles bornées et
déterministes : un for i in 0..n sur une plage ou une collection de taille connue
a un temps d'exécution prévisible. Un loop ou un while dont la condition de
sortie dépend d'une convergence numérique (filtrage, recherche de racine, stabilisation) peut, en
théorie, ne jamais se terminer dans le budget de temps imparti à une tâche périodique : bornez toujours
ces boucles avec un compteur maximal d'itérations, quitte à sortir en échec explicite plutôt que
de dépasser l'échéance (deadline) de la boucle de contrôle.
6. Fil rouge : conversions et classification d'angles
Reprenons le fil rouge du cours : un petit module de traitement d'angles et de capteurs, tel qu'on pourrait en écrire pour un bras robotique ou une base mobile.
use std::f64::consts::PI;
/// Convertit un angle de degrés vers radians.
fn deg_to_rad(deg: f64) -> f64 {
deg * PI / 180.0
}
/// Ramène un angle (en radians) dans l'intervalle [-π, π],
/// utile après une accumulation d'angle sur plusieurs tours de roue.
fn normaliser_angle(angle: f64) -> f64 {
// rem_euclid garantit un reste toujours positif, contrairement à `%` en Rust
// (comme en C, `%` peut renvoyer un résultat négatif pour un dividende négatif).
let deux_pi = 2.0 * PI;
let reste = angle.rem_euclid(deux_pi); // reste dans [0, 2π)
if reste > PI {
reste - deux_pi
} else {
reste
}
}
enum EtatCapteur {
Ok,
HorsLimite,
}
/// Classe une mesure de capteur selon une plage de fonctionnement nominale.
fn classer_mesure(valeur: f64, min: f64, max: f64) -> EtatCapteur {
match valeur {
v if v >= min && v <= max => EtatCapteur::Ok,
_ => EtatCapteur::HorsLimite,
}
}
fn main() {
let angle_deg = 450.0;
let angle_rad = deg_to_rad(angle_deg);
let angle_norm = normaliser_angle(angle_rad);
println!("{angle_deg}° -> {angle_rad:.3} rad -> normalisé {angle_norm:.3} rad");
match classer_mesure(2.7, 0.0, 2.5) {
EtatCapteur::Ok => println!("capteur dans la plage attendue"),
EtatCapteur::HorsLimite => println!("capteur hors limite !"),
}
}
Notez le motif v if condition => dans classer_mesure : c'est une
garde de motif (match guard), qui ajoute une condition booléenne arbitraire
au filtrage. On aurait aussi pu écrire normaliser_angle avec une boucle
while angle > PI { angle -= deux_pi } / while angle < -PI { angle += deux_pi } —
correct mathématiquement, mais non borné en nombre d'itérations si l'angle d'entrée est énorme
(accumulation d'erreurs sur une longue mission). La version avec rem_euclid est préférable
en temps réel car elle s'exécute en temps constant, quelle que soit la valeur d'entrée.
Exercices
Exercice 1 — Normaliser un angle
Écrivez une fonction fn normaliser_angle(angle: f64) -> f64 qui ramène n'importe quel
angle en radians dans l'intervalle [-π, π]. Testez-la avec des valeurs comme
3.5 * PI et -4.0 * PI.
Voir la solution
use std::f64::consts::PI;
fn normaliser_angle(angle: f64) -> f64 {
let deux_pi = 2.0 * PI;
let reste = angle.rem_euclid(deux_pi); // toujours dans [0, 2π)
if reste > PI {
reste - deux_pi
} else {
reste
}
}
fn main() {
println!("{:.4}", normaliser_angle(3.5 * PI)); // -1.5708
println!("{:.4}", normaliser_angle(-4.0 * PI)); // -0.0000 (zéro signé, mathématiquement égal à 0)
}
rem_euclid évite le piège du reste négatif de l'opérateur % et exécute
l'opération en temps constant, sans boucle — un choix naturel pour du code temps réel.
Exercice 2 — Puissance entière
Écrivez une fonction fn puissance(base: f64, exposant: u32) -> f64 qui calcule
base élevé à la puissance exposant à l'aide d'une boucle for
(sans utiliser f64::powi). Réfléchissez au cas exposant == 0.
Voir la solution
fn puissance(base: f64, exposant: u32) -> f64 {
let mut resultat = 1.0;
for _ in 0..exposant {
resultat *= base;
}
resultat
}
fn main() {
println!("{}", puissance(2.0, 10)); // 1024
println!("{}", puissance(5.0, 0)); // 1 (le cas 0 fonctionne naturellement : 0..0 est vide)
}
La boucle for _ in 0..exposant est bornée par construction : elle s'exécute exactement
exposant fois, jamais plus — un exemple typique de boucle « temps réel friendly ».
Exercice 3 — Classer un état de capteur
Définissez une énumération enum EtatCapteur { Ok, HorsLimite } et une fonction
fn diagnostiquer(valeur: f64, min: f64, max: f64) -> EtatCapteur qui utilise
match avec une garde de motif pour renvoyer EtatCapteur::Ok si
valeur est dans [min, max], sinon EtatCapteur::HorsLimite.
Affichez le résultat avec un second match exhaustif.
Voir la solution
enum EtatCapteur {
Ok,
HorsLimite,
}
fn diagnostiquer(valeur: f64, min: f64, max: f64) -> EtatCapteur {
match valeur {
v if v >= min && v <= max => EtatCapteur::Ok,
_ => EtatCapteur::HorsLimite,
}
}
fn main() {
let etat = diagnostiquer(37.2, 0.0, 40.0);
match etat {
EtatCapteur::Ok => println!("température nominale"),
EtatCapteur::HorsLimite => println!("alerte : température hors limite"),
}
}
Si l'on ajoutait plus tard une variante EtatCapteur::Deconnecte, le second
match refuserait de compiler tant qu'il ne la traite pas explicitement (ou via
_) — c'est l'exhaustivité de match au travail.
Exercice 4 — Somme avec for
Écrivez une fonction fn somme_carres(n: u32) -> u32 qui calcule la somme des carrés
de 0 à n inclus (0² + 1² + ... + n²) à l'aide d'un
for sur une plage inclusive.
Voir la solution
fn somme_carres(n: u32) -> u32 {
let mut total: u32 = 0;
for i in 0..=n {
total += i * i;
}
total
}
fn main() {
println!("{}", somme_carres(3)); // 0+1+4+9 = 14
}
La plage 0..=n inclut n, contrairement à 0..n. Sur des
entrées volontairement grandes, méfiez-vous d'un dépassement de capacité (overflow) sur
u32 — un sujet abordé plus en détail au chapitre sur la gestion des erreurs.
Récapitulatif
- Une fonction se déclare avec
fn nom(param: Type) -> TypeRetour { ... }; les paramètres exigent un type explicite. - Le dernier bloc sans point-virgule d'une fonction est sa valeur de retour ;
returnsert au retour anticipé. if/elseetmatchsont des expressions : on peut les assigner directement à une variable.matchest exhaustif — le compilateur refuse tout cas non couvert, contrairement auswitchdu C.loop(avecbreak valeur),whileetfor(sur des plages0..n/0..=nou des collections) couvrent tous les besoins d'itération ; les étiquettes ('label:) ciblent une boucle précise dans du code imbriqué.- En temps réel, préférez les boucles bornées (
forsur une plage connue) aux boucles dont la sortie dépend d'une convergence numérique non garantie.