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
| Type | Taille | Valeurs |
|---|---|---|
i8, i16, i32, i64, i128 | 1–16 octets | Entiers signés (-2^{n-1} à 2^{n-1} - 1) |
u8, u16, u32, u64, u128 | 1–16 octets | Entiers non signés (0 à 2^n - 1) |
f32, f64 | 4, 8 octets | Flottants IEEE 754 |
bool | 1 octet | true / false |
char | 4 octets | Un 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
| Type | Exemple | Usage |
|---|---|---|
(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 |
String | String::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 pTuple 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 étatMé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 existeExercices (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())
}
}
}
}