iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 4·30 min

Structs, enums et pattern matching

Rust n'a pas de classes. On regroupe des données dans des structs, on décrit des alternatives avec des enums, et on leur attache des méthodes avec impl. Le match fait le lien : il examine une valeur et oblige à traiter chaque cas. Cette combinaison remplace null, les exceptions et une bonne partie de l'héritage.

Les structs

struct Utilisateur {
    nom: String,
    email: String,
    age: u32,
    actif: bool,
}
 
fn main() {
    let mut ada = Utilisateur {
        nom: String::from("Ada"),
        email: String::from("ada@exemple.fr"),
        age: 36,
        actif: true,
    };
    ada.age += 1;
    println!("{} a {} ans", ada.nom, ada.age);
}

La mutabilité s'applique à toute la struct : on ne peut pas rendre un seul champ modifiable.

Deux raccourcis d'écriture reviennent souvent :

fn creer(nom: String, email: String) -> Utilisateur {
    Utilisateur { nom, email, age: 0, actif: true } // nom: nom abrégé
}
 
let bob = Utilisateur {
    nom: String::from("Bob"),
    ..ada // reprendre les autres champs de ada
};

Attention, ..ada déplace les champs non Copy de ada (ici email). ada.email devient inutilisable, la règle du chapitre 3 s'applique champ par champ.

Il existe aussi des structs sans noms de champs, utiles pour donner un type distinct à une valeur simple :

struct Metres(f64);
struct Secondes(f64);

Impossible désormais de passer des secondes à une fonction qui attend des mètres, même si les deux sont des f64.

Les méthodes : impl

#[derive(Debug)]
struct Rectangle {
    largeur: f64,
    hauteur: f64,
}
 
impl Rectangle {
    // fonction associée (pas de self) : un constructeur
    fn carre(cote: f64) -> Self {
        Self { largeur: cote, hauteur: cote }
    }
 
    // méthode qui lit
    fn aire(&self) -> f64 {
        self.largeur * self.hauteur
    }
 
    // méthode qui modifie
    fn agrandir(&mut self, facteur: f64) {
        self.largeur *= facteur;
        self.hauteur *= facteur;
    }
 
    fn peut_contenir(&self, autre: &Rectangle) -> bool {
        self.largeur >= autre.largeur && self.hauteur >= autre.hauteur
    }
}
 
fn main() {
    let mut r = Rectangle { largeur: 3.0, hauteur: 2.0 };
    let c = Rectangle::carre(1.5);
    r.agrandir(2.0);
    println!("{r:?}, aire {}, contient c : {}", r.aire(), r.peut_contenir(&c));
}

Le premier paramètre dit comment la méthode utilise l'objet, avec exactement les règles du chapitre précédent : &self emprunte en lecture, &mut self en écriture, self prend possession (l'objet n'est plus utilisable après l'appel). Self désigne le type en cours.

#[derive(Debug)] demande au compilateur de générer le code d'affichage {:?}. On y reviendra avec les traits au chapitre 6.

Il n'y a pas de constructeur spécial en Rust. La convention est une fonction associée new.

Les enums

Un enum Rust est bien plus qu'une liste de constantes : chaque variante peut porter ses propres données.

enum Forme {
    Cercle { rayon: f64 },
    Rectangle { largeur: f64, hauteur: f64 },
    Triangle(f64, f64, f64),
}
 
impl Forme {
    fn aire(&self) -> f64 {
        match self {
            Forme::Cercle { rayon } => std::f64::consts::PI * rayon * rayon,
            Forme::Rectangle { largeur, hauteur } => largeur * hauteur,
            Forme::Triangle(a, b, c) => {
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
}
 
fn main() {
    let formes = [
        Forme::Cercle { rayon: 1.0 },
        Forme::Rectangle { largeur: 2.0, hauteur: 3.0 },
        Forme::Triangle(3.0, 4.0, 5.0),
    ];
    for f in &formes {
        println!("{:.2}", f.aire());
    }
}

Une valeur de type Forme est forcément l'une des trois variantes, avec les données correspondantes. Dans un langage objet, on aurait une classe abstraite et trois sous-classes. Ici, tout tient en un type, et le match voit tous les cas d'un coup.

match est exhaustif

Si on ajoute une variante Hexagone à l'enum, chaque match qui ne la traite pas devient une erreur de compilation :

error[E0004]: non-exhaustive patterns: `&Forme::Hexagone { .. }` not covered

C'est la fonction la plus précieuse de Rust au quotidien. Ajouter un cas à un type ne laisse jamais de code oublié quelque part : le compilateur vous donne la liste des endroits à mettre à jour.

match sait faire bien plus que comparer des variantes :

fn decrire(n: i32) -> &'static str {
    match n {
        0 => "zéro",
        1 | 2 | 3 => "petit",
        4..=9 => "moyen",
        x if x < 0 => "négatif",
        _ => "grand",
    }
}
 
fn main() {
    for n in [0, 2, 7, -5, 100] {
        println!("{n} : {}", decrire(n));
    }
}

| combine des motifs, 4..=9 teste une plage, if ajoute une condition (une « garde ») et _ attrape tout le reste. Les branches sont testées dans l'ordre.

Option : la fin de null

Rust n'a pas de null. Une valeur qui peut être absente a le type Option<T>, un enum de la bibliothèque standard :

enum Option<T> {
    Some(T),
    None,
}
fn trouver_age(nom: &str) -> Option<u32> {
    match nom {
        "Ada" => Some(36),
        "Linus" => Some(55),
        _ => None,
    }
}
 
fn main() {
    match trouver_age("Ada") {
        Some(age) => println!("Ada a {age} ans"),
        None => println!("inconnue"),
    }
}

Un Option<u32> n'est pas un u32. Impossible de faire trouver_age("Ada") + 1 : le compilateur oblige à traiter le cas None avant d'utiliser la valeur. Le fameux « erreur d'un milliard de dollars », la référence nulle déréférencée, ne peut pas arriver.

Quelques méthodes évitent d'écrire un match à chaque fois :

fn main() {
    let age: Option<u32> = Some(36);
    let rien: Option<u32> = None;
 
    println!("{}", age.unwrap_or(0));          // 36
    println!("{}", rien.unwrap_or(0));         // 0
    println!("{:?}", age.map(|a| a + 1));      // Some(37)
    println!("{}", age.is_some());             // true
    // rien.unwrap();  // panique : à éviter hors des tests et prototypes
}

if let et let else

Quand un seul cas vous intéresse, match est verbeux. if let teste un motif :

fn main() {
    let config_port: Option<u16> = Some(8080);
    if let Some(port) = config_port {
        println!("écoute sur {port}");
    }
}

let else fait l'inverse : il extrait la valeur ou sort de la fonction. C'est idéal pour les vérifications en début de fonction :

fn saluer(nom: Option<&str>) {
    let Some(nom) = nom else {
        println!("personne à saluer");
        return;
    };
    println!("bonjour {nom}");
}
 
fn main() {
    saluer(Some("Ada"));
    saluer(None);
}

Le code principal reste à gauche, sans s'enfoncer dans des niveaux d'indentation.

Exercice

Modélisez les feux d'un carrefour avec un enum Feu (Rouge, Orange, Vert), et écrivez deux méthodes : suivant(&self) -> Feu et duree(&self) -> u32 (en secondes : 30, 5, 25). Faites tourner le feu pendant un cycle complet.

#[derive(Debug, Clone, Copy)]
enum Feu {
    Rouge,
    Orange,
    Vert,
}
 
impl Feu {
    fn suivant(&self) -> Feu {
        match self {
            Feu::Rouge => Feu::Vert,
            Feu::Vert => Feu::Orange,
            Feu::Orange => Feu::Rouge,
        }
    }
 
    fn duree(&self) -> u32 {
        match self {
            Feu::Rouge => 30,
            Feu::Orange => 5,
            Feu::Vert => 25,
        }
    }
}
 
fn main() {
    let mut feu = Feu::Rouge;
    for _ in 0..3 {
        println!("{feu:?} pendant {} s", feu.duree());
        feu = feu.suivant();
    }
}

Structs pour regrouper, enums pour les alternatives, match pour ne jamais oublier un cas, Option à la place de null. Ce modèle de données est l'un des points forts de Rust, et il prépare la gestion d'erreurs du chapitre suivant, qui repose sur un autre enum : Result.

Précédent
Ownership et emprunts
Suivant
Collections et gestion d'erreurs

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main