backend / rust
Structs et enums
Explication
Ce que vous allez apprendre
- Définir une struct et lui attacher des méthodes via un bloc
impl - Utiliser
#[derive(...)]pour générer automatiquement des implémentations courantes - Créer un enum dont chaque variante peut porter des données différentes
- Utiliser
matchpour traiter chaque variante d'un enum de façon exhaustive - Comprendre
Option<T>, l'enum de la stdlib qui remplacenull
Dans quel contexte ?
Un développeur qui vient de Java cherche le mot-clé class pour modéliser un Utilisateur, et découvre à la place struct Utilisateur couplée à un bloc impl séparé pour ses méthodes. Il découvre ensuite un enum Forme dont chaque variante (Cercle, Rectangle, Triangle) porte ses propres champs — une possibilité qu'un simple enum Java ou C ne propose pas nativement, et qui change profondément la façon de modéliser des données en Rust.
D'abord, une struct regroupe des données, un impl leur attache un comportement
struct Utilisateur { nom: String, email: String, age: u32 } déclare uniquement la forme des données. Le comportement associé s'écrit séparément dans un bloc impl Utilisateur { ... }, où fn nouveau(...) -> Self (sans self en paramètre) joue le rôle d'un constructeur/factory, et où fn description(&self) -> String (avec &self) emprunte l'instance pour la lire.
Une fois cette séparation acquise, un raccourci très pratique s'impose
Écrire manuellement Debug, Clone ou PartialEq pour chaque struct serait fastidieux et source d'erreurs. #[derive(Debug, Clone)], placé juste au-dessus de la déclaration, génère automatiquement ces implémentations standard, à condition que tous les champs de la struct les supportent également.
| Trait dérivable | Ce qu'il apporte |
|---|---|
Debug | Permet {:?} dans println!, essentiel pour déboguer |
Clone | Permet .clone(), une duplication explicite |
PartialEq | Permet de comparer deux instances avec == |
PartialOrd | Permet de comparer avec <, > (ordre) |
Prérequis
Il faut être à l'aise avec l'ownership et le borrowing (leçons précédentes) : la distinction entre une méthode &self (emprunte) et self (consomme) en dépend directement.
Il reste un concept bien plus puissant qu'un simple enum d'autres langages : le type somme
Un enum Rust peut porter des données différentes pour chaque variante, comme Forme::Cercle { rayon: f64 } face à Forme::Rectangle { largeur: f64, hauteur: f64 }. C'est ce qu'on appelle un "type somme" : la valeur est exactement UNE des variantes possibles, jamais un mélange, et chaque variante peut avoir sa propre structure de données interne.
Piège fréquent
Oublier de traiter une variante dans un match sur un enum ne compile tout simplement pas : Rust exige l'exhaustivité (tous les cas couverts) sauf si un _ de secours est présent. C'est une contrainte au début, mais elle garantit qu'ajouter une nouvelle variante à un enum plus tard forcera le compilateur à signaler tous les endroits du code qui doivent être mis à jour en conséquence.
Enfin, l'enum le plus utilisé de tout l'écosystème Rust : Option<T>
Rust n'a pas de null : l'absence potentielle d'une valeur se représente explicitement par Option<T>, qui vaut soit Some(valeur) soit None. Cette approche force le développeur à traiter le cas d'absence explicitement (via match ou des combinateurs), éliminant structurellement toute la classe de bugs "NullPointerException" si fréquente ailleurs.
Bonne pratique
Ajoute systématiquement #[derive(Debug)] à chaque struct et enum que tu écris, même en prototype rapide : ce dérive quasi gratuit permet d'afficher n'importe quelle valeur avec {:?} pendant le débogage, sans jamais avoir à l'implémenter manuellement.
Maintenant que tu sais modéliser des données avec des enums riches, la prochaine leçon approfondit l'outil qui les exploite pleinement : le pattern matching avec match.
Commandes & code
Structs et enums
Les structs modélisent des données, les enums modélisent des choix — souvent combinés.
#[derive(Debug, Clone)] // derive genere automatiquement des implementations utiles
struct Utilisateur {
nom: String,
email: String,
age: u32,
actif: bool,
}
impl Utilisateur {
// Fonction associee (pas de &self) : equivalent d'un constructeur / factory
fn nouveau(nom: &str, email: &str, age: u32) -> Self {
Self { nom: nom.to_string(), email: email.to_string(), age, actif: true }
}
// Methode avec &self : emprunte, ne consomme pas l'instance
fn description(&self) -> String {
format!("{} ({}), {} ans", self.nom, self.email, self.age)
}
// Methode avec &mut self : peut modifier l'instance
fn desactiver(&mut self) {
self.actif = false;
}
}
// Tuple struct : struct sans noms de champs, utile pour du typage fort leger
struct Point(f64, f64);
// Unit struct : sans donnees, souvent utilisee comme marqueur de trait
struct Marqueur;
// Enum : type somme, chaque variante peut porter des donnees differentes
#[derive(Debug)]
enum Forme {
Cercle { rayon: f64 },
Rectangle { largeur: f64, hauteur: f64 },
Triangle { base: f64, hauteur: 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 { base, hauteur } => 0.5 * base * hauteur,
}
}
}
// Enum tres celebre de la stdlib : Option<T> pour representer "peut-etre une valeur"
fn diviser(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
let mut u = Utilisateur::nouveau("Alice", "alice@example.com", 30);
println!("{}", u.description());
u.desactiver();
println!("{:?}", u); // Debug derive permet {:?}
let u2 = u.clone(); // Clone derive permet .clone()
println!("{}", u2.nom);
let p = Point(3.0, 4.0);
println!("{} {}", p.0, p.1);
let formes = vec![
Forme::Cercle { rayon: 2.0 },
Forme::Rectangle { largeur: 3.0, hauteur: 4.0 },
Forme::Triangle { base: 5.0, hauteur: 6.0 },
];
for f in &formes {
println!("{:?} -> aire = {:.2}", f, f.aire());
}
match diviser(10.0, 2.0) {
Some(r) => println!("resultat: {}", r),
None => println!("division impossible"),
}
}Résumé
#[derive(Debug, Clone, ...)]génère automatiquement des implémentations standard sans code manuel.- Un
enumpeut porter des données différentes par variante — bien plus riche qu'un enum C classique. implsépare les données (struct/enum) de leur comportement (méthodes).Option<T>remplacenull: l'absence de valeur est explicite et vérifiée à la compilation.
Exercices pratiques
Mission : modeliser un catalogue de formes pour un moteur de rendu
Objectif : Concevoir un enum de formes riche en donnees et une methode exhaustive, puis diagnostiquer une struct mal derivee.
Contexte
L'equipe graphique d'un jeu veut ajouter une nouvelle forme, Ellipse { rayon_x: f64, rayon_y: f64 }, a l'enum Forme vu en cours. Un dev a rapidement ajoute la variante mais oublie de mettre a jour la methode aire, et le code ne compile plus. Un autre dev se plaint separement que println!("{:?}", ma_forme) ne fonctionne pas sur sa propre struct Cercle a lui.