Phase 3Structures de données

#10 Enums et pattern matching

enum, match, if let

Définir des enums

Un enum (énumération) permet de définir un type qui peut être l'un de plusieurs variants. C'est parfait pour modéliser des données qui peuvent prendre différentes formes.

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

let d = Direction::Nord;

Chaque variant est un constructeur du type Direction. On y accède avec la syntaxe ::.

Enums avec données

Contrairement à d'autres langages, les enums de Rust peuvent contenir des données. Chaque variant peut avoir son propre type de données.

Enum avec données
enum AdresseIp {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let locale = AdresseIp::V4(127, 0, 0, 1);
    let loopback = AdresseIp::V6(String::from("::1"));

    afficher_adresse(locale);
}

fn afficher_adresse(adresse: AdresseIp) {
    match adresse {
        AdresseIp::V4(a, b, c, d) => {
            println!("Adresse IPv4 : {a}.{b}.{c}.{d}");
        }
        AdresseIp::V6(s) => {
            println!("Adresse IPv6 : {s}");
        }
    }
}

Chaque variant peut contenir différents types : des tuples, des structs anonymes, ou même rien du tout. C'est beaucoup plus expressif qu'un simple enum d'entiers.

Variants mixtes
enum Message {
    Quitter,                       // Pas de données
    Deplacer { x: i32, y: i32 },   // Struct anonyme
    Ecrire(String),                // Un String
    ChangerCouleur(u8, u8, u8),    // Un tuple
}

L'expression match

match est l'outil principal pour travailler avec les enums. Le compilateur vous oblige à gérer tous les variants — impossible d'en oublier un.

match exhaustif
enum Forme {
    Cercle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64),
}

fn aire(forme: Forme) -> f64 {
    match forme {
        Forme::Cercle(rayon) => {
            std::f64::consts::PI * rayon * rayon
        }
        Forme::Rectangle(l, h) => l * h,
        Forme::Triangle(base, hauteur) => base * hauteur / 2.0,
    }
}

Le match est une expression : il retourne une valeur. Chaque branche utilise => pour séparer le motif de l'action. L'opérateur _ capture tous les cas restants.

La syntaxe if let

Quand vous ne vous intéressez qu'à un seul variant, if let est plus concis qu'un match complet :

if let
let valeur: Option<i32> = Some(42);

// Avec match (verbeux)
match valeur {
    Some(n) => println!("Le nombre est : {n}"),
    None => (),
}

// Avec if let (concis)
if let Some(n) = valeur {
    println!("Le nombre est : {n}");
}

if let est du sucre syntaxique pour un match qui ne traite qu'un seul cas. Vous pouvez ajouter un else pour gérer les autres cas.

Enums courants (Option, Result)

Deux enums sont omniprésents en Rust et inclus dans le prélude (disponibles sans import) :

Option<T>
// Défini dans la bibliothèque standard :
// enum Option<T> {
//     Some(T),
//     None,
// }

fn diviser(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 {
        None // Division par zéro impossible
    } else {
        Some(a / b)
    }
}

fn main() {
    match diviser(10.0, 3.17) {
        Some(resultat) => println!("Résultat : {resultat:.2}"),
        None => println!("Division par zéro !"),
    }
}

Option remplace les valeurs null des autres langages. En Rust, si une valeur peut être absente, elle est de type Option<T> — le compilateur vous oblige à gérer le cas None.

Result<T, E>
// Défini dans la bibliothèque standard :
// enum Result<T, E> {
//     Ok(T),
//     Err(E),
// }

use std::fs;

fn main() {
    let contenu = fs::read_to_string("fichier.txt");

    match contenu {
        Ok(texte) => println!("Lecture du fichier réussie : {texte}"),
        Err(e) => println!("Erreur de lecture : {e}"),
    }
}

Result est utilisé pour les opérations qui peuvent échouer. Ok(T) contient la valeur en cas de succès, Err(E) contient l'erreur. C'est la base de la gestion d'erreurs en Rust — pas d'exceptions, juste des types.

À vous de jouer

Essayez les commandes ci-dessous pour voir les enums en action :

terminal — cargo
user@stemlegacy:~/enums-demo$