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

Contrôle de flux et pattern matching

Rust propose les structures de contrôle :

  • if (then else if else),
  • match

et les structures de boucles avec ou sans contrôle :

  • for,
  • while et
  • loop.

En Rust, le contrôle de flux est majoritairement constitué d’expressions : chaque bloc peut retourner une valeur.

L’expression if

if est une expression. Cela signifie qu’un bloc if retourne une valeur et peut être utilisé pour initialiser une variable.

let condition = true;

// Les branches doivent impérativement retourner le même type
let nombre = if condition { 5 } else { 6 };
  • Sécurité : Il n’y a pas de conversion implicite en booléen. La condition doit être un booléen strict (if nombre != 0 et non if nombre).
  • Robustesse : Si nous utilisons if comme expression, le bloc else est obligatoire.

Les boucles : loop, while et for

Rust dispose de trois mécanismes de boucle qui intègrent des garanties de robustesse.

loop et le retour de valeur

loop crée une boucle infinie. C’est l’outil idéal pour les sondages (polling) ou les serveurs. Elle permet de retourner une valeur via le mot-clé break.

let mut compteur = 0;

let resultat = loop {
    compteur += 1;
    if compteur == 10 {
        break compteur * 2; // Retourne la valeur
    }
};

for et la propriété (Iterator)

La boucle for est la méthode la plus sûre pour parcourir une collection, car elle évite les erreurs d’indice hors limites (out of bounds). Elle consomme ou emprunte la collection selon le type d’itérateur fourni.

let elements = vec!["a", "b", "c"];

// Emprunt immuable (&elements) pour pouvoir réutiliser le vecteur par la suite
for el in &elements {
    println!("{el}");
}

Le pattern matching avec match

Le match est un des piliers de la robustesse en Rust.

Il permet de comparer une valeur à une série de motifs (patterns).

enum Direction {
    Nord,
    Sud,
    Est,
    Ouest,
}

let cap = Direction::Nord;

match cap {
    Direction::Nord => println!("Vers le Nord"),
    Direction::Sud => println!("Vers le Sud"),
    Direction::Est | Direction::Ouest => println!("Sur les côtés"),
}

La règle absolue : L’exhaustivité

Le compilateur Rust impose que tous les cas possibles soient couverts. Si nous oublions une variante d’une structure ou d’un enum, le code ne passe pas l’étape de compilation.

Pour capturer tous les cas restants, nous pouvons utiliser le motif universel _ :

let valeur = 7;

match valeur {
    1 => println!("Un"),
    3 => println!("Trois"),
    _ => println!("Autre nombre"), // Obligatoire pour couvrir tous les i32 possibles
}

if let

Lorsque nous ne voulons gérer qu’un seul motif et ignorer les autres, match peut s’avérer verbeux. Rust propose if let pour simplifier ce flux. C’est particulièrement utile avec les types Option et Result.

let configuration = Some(42);

// Au lieu d'un match exhaustif à deux branches :
if let Some(valeur) = configuration {
    println!("La configuration est : {valeur}");
}

if let est très utilisé pour extraire et lier une valeur contenue dans un type énuméré (comme Some(valeur) pour Option ou Ok(valeur) pour Result) seulement si le motif correspond.

Comparaison de deux structures d'affectation protégées

if let Some(valeur) = configuration

Si la configuration est Some, le programme exécute le bloc de code avec la valeur. Si c’est None, il ignore le bloc et le programme continue normalement son exécution en dessous.

#![allow(unused)]
fn main() {
// Avec if let : le programme continue quoi qu'il arrive
if let Some(valeur) = configuration {
    println!("Config trouvée : {}", valeur);
}
println!("Ce message est toujours affiché");
}

let valeur = configuration?;

? est un opérateur de propagation (early return). Si la configuration est Some, il extrait la valeur et l’affecte. Mais si c’est None, le programme arrête immédiatement l’exécution de toute la fonction actuelle et retourne None à la fonction appelante.

Cette structure n’est utilisable qu’à l’intérieur d’une fonction qui retourne elle-même un type Option (ou un Result si on utilise ? sur un Result) et lorsque la logique implémentée donne du sens à cette utilisation.

#![allow(unused)]
fn main() {
let valeur = configuration?; 
println!("Ce message n'est affiché que si configuration est Some");
}

let else

La syntaxe let else permet d’extraire la valeur d’une énumération (comme Option ou Result) en assignant directement le contenu à une variable dans la portée courante. Si le motif ne correspond pas (cas d’échec), le bloc else est exécuté et doit obligatoirement interrompre le flux courant (via un return, break, continue ou panic!).

C’est l’outil idéal pour appliquer le patron de conception (design pattern) de retour anticipé (early return).

Il permet d’éviter d’imbriquer le code principal dans des blocs if let ou match successifs.

Exemple concret d’utilisation

Nous écrivons une fonction qui doit extraire l’identifiant d’un utilisateur à partir d’une configuration, uniquement si cet utilisateur est actif.

Sans let else

#![allow(unused)]
fn main() {
fn traiter_utilisateur(config: &Configuration) -> Option<u32> {
    if let Some(utilisateur) = &config.utilisateur {
        if utilisateur.actif {
            Some(utilisateur.id)
        } else {
            None
        }
    } else {
        None
    }
}
}

Avec let else

#![allow(unused)]
fn main() {
struct Utilisateur {
    id: u32,
    actif: bool,
}

struct Configuration {
    utilisateur: Option<Utilisateur>,
}

fn traiter_utilisateur(config: &Configuration) -> Option<u32> {
    let Some(utilisateur) = &config.utilisateur else {
        return None; // On quitte la fonction (divergence obligatoire)
    };

    if !utilisateur.actif {
        return None;
    }

    Some(utilisateur.id)
}
}

Les règles strictes du let else

  • Variables exportées : Contrairement à un if let, les variables liées dans le motif du let (ici utilisateur) sont valides après le bloc let else, pour le reste de la portée en cours (scope).
  • Divergence obligatoire : Le bloc else doit être divergeant (avoir le type primitif !). Cela signifie que nous ne pouvons pas simplement y mettre une valeur ou y laisser un bloc vide ; nous devons quitter la fonction (return), la boucle (break/continue) ou faire crasher le thread (panic!).