2. Types, Structs, Enums, Pattern Matching

Le système de types de Rust est expressif et sûr. Les enums avec données (ADT — Algebraic Data Types) et le pattern matching sont au cœur de la gestion d’erreur et de la logique métier.


2.1 Types Primitifs

Scalaires

TypeTailleValeurs
i8, i16, i32, i64, i1281–16 octetsEntiers signés (-2^{n-1} à 2^{n-1} - 1)
u8, u16, u32, u64, u1281–16 octetsEntiers non signés (0 à 2^n - 1)
f32, f644, 8 octetsFlottants IEEE 754
bool1 octettrue / false
char4 octetsUn caractère Unicode
let x: i32 = -42;
let y: u64 = 42;
let pi: f64 = 3.14159;
let is_ok: bool = true;
let letter: char = '🦀';

Composites

TypeExempleUsage
(i32, f64, &str)(42, 3.14, "hello")Tuple — regroupe des valeurs de types différents
[f64; 3][1.0, 2.0, 3.0]Tableau — taille fixe connue à la compilation
Vec<T>vec![1, 2, 3]Vecteur — taille dynamique (heap)
&str"hello"Slice de chaîne — référence immutable
StringString::from("hello")Chaîne propriétaire (heap)
let tuple = (42, 3.14, "hello");
let (a, b, c) = tuple;       // destructuring
 
let array: [i32; 3] = [1, 2, 3];
let first = array[0];        // accès par index
 
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.pop();                   // Some(2)

2.2 Structs

Struct classique (named fields)

struct Point {
    x: f64,
    y: f64,
}
 
let p = Point { x: 1.0, y: 2.0 };
println!("({}, {})", p.x, p.y);
 
// Struct update syntax
let p2 = Point { x: 3.0, ..p };  // copie y depuis p

Tuple struct (newtype pattern)

struct Couleur(u8, u8, u8);  // un tuple nommé
 
let rouge = Couleur(255, 0, 0);
println!("R: {}, G: {}, B: {}", rouge.0, rouge.1, rouge.2);
 
// Newtype : envelopper un type existant pour lui donner un sens
struct GradientId(usize);
struct WorkerId(usize);  // même type sous-jacent, mais incompatibles !

Unit struct (pas de données)

struct GradientAggregator;  // comme un trait, mais sans état

Méthodes avec impl

struct Rectangle {
    width: f64,
    height: f64,
}
 
impl Rectangle {
    // Constructeur (method)
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }
 
    // Méthode sur &self (lecture seule)
    fn area(&self) -> f64 {
        self.width * self.height
    }
 
    // Méthode sur &mut self (modification)
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }
 
    // Méthode qui prend ownership
    fn into_tuple(self) -> (f64, f64) {
        (self.width, self.height)
    }
}
 
let mut rect = Rectangle::new(3.0, 4.0);
println!("aire: {}", rect.area());
rect.scale(2.0);
let (w, h) = rect.into_tuple();

2.3 Enums — Le cœur de la gestion de cas

Enum simple

enum Direction {
    North,
    South,
    East,
    West,
}
 
let d = Direction::North;

Enum avec données (ADT — Algebraic Data Type)

Chaque variante peut contenir des données de types différents. C’est ce qui rend les enums Rust si puissants.

enum Message {
    Quit,                       // aucune donnée
    Move { x: i32, y: i32 },   // struct anonyme
    Write(String),             // tuple
    ChangeColor(i32, i32, i32), // tuple
}
 
let msg = Message::Move { x: 10, y: 20 };
let msg = Message::Write(String::from("hello"));

L’enum le plus important : Option<T>

Remplaçant de null. Soit une valeur existe, soit elle n’existe pas.

enum Option<T> {
    Some(T),   // une valeur de type T
    None,      // pas de valeur
}
 
fn trouver_mot(mots: &[&str], cible: &str) -> Option<usize> {
    for (i, &mot) in mots.iter().enumerate() {
        if mot == cible {
            return Some(i);
        }
    }
    None
}
 
let mots = vec!["hello", "world"];
match trouver_mot(&mots, "world") {
    Some(i) => println!("trouvé à l'index {i}"),
    None => println!("pas trouvé"),
}

L’enum pour l’erreur : Result<T, E>

enum Result<T, E> {
    Ok(T),    // succès avec valeur
    Err(E),   // échec avec erreur
}
 
fn diviser(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("division par zéro"))
    } else {
        Ok(a / b)
    }
}

2.4 Pattern Matching — match et if let

match — équivalent du switch, mais exhaustif

enum Status {
    Actif,
    Inactif { since: u64 },
    Erreur(String),
}
 
fn decrire(status: Status) -> String {
    match status {
        Status::Actif => String::from("actif"),
        Status::Inactif { since } => format!("inactif depuis {since}"),
        Status::Erreur(msg) => format!("erreur : {msg}"),
    }
}

Exhaustivité

Le compilateur vérifie que tous les cas sont couverts :

fn direction_to_int(d: Direction) -> i32 {
    match d {
        Direction::North => 0,
        Direction::East  => 90,
        Direction::South => 180,
        // ⛔ oubli de West : le compilateur refuse
    }
}

if let pour un seul cas

let config_max = Some(3u8);
 
// match lourd pour un seul cas :
match config_max {
    Some(max) => println!("max: {max}"),
    None => (),  // obligatoire mais inutile
}
 
// if let : plus concis
if let Some(max) = config_max {
    println!("max: {max}");
}

Pattern matching avancé

let nombre = 42;
 
match nombre {
    0 => println!("zéro"),
    1..=10 => println!("petit"),
    n if n % 2 == 0 => println!("pair"),
    _ => println!("autre"),  // _ = tout le reste (wildcard)
}
 
// Déstructuration
struct Point3D { x: i32, y: i32, z: i32 }
let p = Point3D { x: 0, y: 1, z: 2 };
 
match p {
    Point3D { x: 0, y, z } => println!("sur l'axe yz: {y}, {z}"),
    Point3D { x, .. } => println!("x = {x}"),   // .. ignore le reste
}

2.5 Conversions et Into/From

From et Into — conversions réciproques

struct Point {
    x: f64,
    y: f64,
}
 
// De (f64, f64) vers Point
impl From<(f64, f64)> for Point {
    fn from((x, y): (f64, f64)) -> Self {
        Self { x, y }
    }
}
 
// Utilisation
let p = Point::from((3.0, 4.0));
let p: Point = (3.0, 4.0).into();  // Into est automatique si From existe

Exercices (rustlings)

// 1. Compléter l'évaluation
enum Evaluation {
    Note(i32),
    Mot(String),
}
 
fn afficher(e: Evaluation) {
    match e {
        // ??
    }
}
 
// 2. Implémenter une pile avec Option
struct Stack<T> {
    items: Vec<T>,
}
 
impl<T> Stack<T> {
    fn pop(&mut self) -> Option<T> {
        // ??
    }
}
 
// 3. Corriger le pattern matching
fn decrire_point(p: (i32, i32)) -> String {
    match p {
        (0, _) => "sur l'axe y",
        (_, 0) => "sur l'axe x",
        (x, y) if x == y => "sur la diagonale",
        (x, y) => format!("({x}, {y})"),
    }
}

2.6 Pattern Matching Avancé

Guards (if dans les branches)

fn classer_gradient(norm: f64, dim: usize) -> &'static str {
    match (norm, dim) {
        (n, _) if n.is_nan() => "invalide",
        (0.0, _) => "nul",
        (n, d) if n > (d as f64).sqrt() => "anormalement grand",
        _ => "normal",
    }
}

@ bindings — capturer ET matcher

enum Gradient {
    Valeur(f64),
    Batch(Vec<f64>),
}
 
fn analyser(g: Gradient) {
    match g {
        Gradient::Valeur(v @ 0.0..=1.0) => println!("petit gradient: {v}"),
        Gradient::Valeur(v) => println!("grand gradient: {v}"),
        Gradient::Batch(v) if v.len() > 1000 => println!("gros batch: {}", v.len()),
        Gradient::Batch(ref v) => println!("batch standard: {}", v.len()),
    }
}

matches! macro et let else

let g = Some(42.0);
 
// matches! — test rapide
assert!(matches!(g, Some(v) if v > 0.0));
 
// let else — déstructuration ou return/break/panic
fn process(opt: Option<Vec<f64>>) -> Vec<f64> {
    let Some(grads) = opt else {
        return vec![];
    };
    grads  // ici on a accès à grads
}

Enums récursifs avec Box

enum ArbreGradient {
    Feuille(f64),
    Nœud {
        gauche: Box<ArbreGradient>,
        droite: Box<ArbreGradient>,
        op: fn(f64, f64) -> f64,
    }
}
 
impl ArbreGradient {
    fn evaluer(&self) -> f64 {
        match self {
            ArbreGradient::Feuille(v) => *v,
            ArbreGradient::Nœud { gauche, droite, op } => {
                op(gauche.evaluer(), droite.evaluer())
            }
        }
    }
}

🔗 Voir aussi