1. Les structures
Une structure regroupe plusieurs valeurs nommées qui forment un tout.
#[derive(Debug)]
struct Poste {
nom: String,
salle: u16,
ram_go: u32,
actif: bool,
}
fn main() {
let p = Poste {
nom: String::from("PC-207-01"),
salle: 207,
ram_go: 16,
actif: true,
};
println!("{} est en salle {}", p.nom, p.salle);
println!("{:?}", p); // grâce à #[derive(Debug)]
println!("{:#?}", p); // version indentée
}
- Les noms de structures s’écrivent en
CamelCase, les champs ensnake_case. #[derive(Debug)]demande au compilateur de générer l’affichage de débogage{:?}.- Pour modifier un champ, l’instance entière doit être
mut:let mut p = …; p.actif = false;.
struct Poste { nom: String, salle: u16, ram_go: u32, actif: bool }
fn nouveau_poste(nom: String, salle: u16) -> Poste {
Poste { nom, salle, ram_go: 8, actif: true } // raccourci : nom au lieu de nom: nom
}
fn main() {
let p1 = nouveau_poste(String::from("PC-207-01"), 207);
let p2 = Poste {
nom: String::from("PC-207-02"),
..p1 // les autres champs viennent de p1
};
println!("{} {} {}", p2.nom, p2.salle, p2.ram_go);
}
Structures-tuples
struct Couleur(u8, u8, u8);
struct Metres(f64);
fn main() {
let rouge = Couleur(255, 0, 0);
let distance = Metres(42.5);
println!("{} {}", rouge.0, distance.0);
}
Une structure-tuple à un seul champ (Metres) empêche de confondre des valeurs de même type mais de sens différent : on ne peut pas passer des mètres à une fonction qui attend des secondes. C’est le motif newtype, très utilisé.
2. Les méthodes
Un bloc impl associe des fonctions à une structure.
#[derive(Debug)]
struct Rectangle {
largeur: f64,
hauteur: f64,
}
impl Rectangle {
// fonction associée (pas de self) : sert de constructeur
fn new(largeur: f64, hauteur: f64) -> Rectangle {
Rectangle { largeur, hauteur }
}
fn carre(cote: f64) -> Self { // Self = le type en cours
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;
}
// méthode qui consomme l'instance
fn en_texte(self) -> String {
format!("{} x {}", self.largeur, self.hauteur)
}
}
fn main() {
let mut r = Rectangle::new(3.0, 2.0);
println!("aire : {}", r.aire());
r.agrandir(2.0);
println!("{:?}", r);
let c = Rectangle::carre(5.0);
println!("{}", c.en_texte());
// println!("{:?}", c); ← erreur : c a été consommé par en_texte
}
| Premier paramètre | Sens | Appel |
|---|---|---|
&self | La méthode lit l’instance | r.aire() |
&mut self | La méthode modifie l’instance | r.agrandir(2.0) (r doit être mut) |
self | La méthode consomme l’instance, qui n’existe plus après | c.en_texte() |
| aucun | Fonction associée, souvent un constructeur | Rectangle::new(3.0, 2.0) |
On retrouve exactement les règles du niveau 2 : &self est un emprunt en lecture, &mut self un emprunt en écriture, self un déplacement.
3. Les énumérations
Une énumération représente une valeur qui est une variante parmi plusieurs.
#[derive(Debug)]
enum Protocole {
Http,
Https,
Ssh,
}
fn port_par_defaut(p: &Protocole) -> u16 {
match p {
Protocole::Http => 80,
Protocole::Https => 443,
Protocole::Ssh => 22,
}
}
fn main() {
let p = Protocole::Ssh;
println!("{:?} utilise le port {}", p, port_par_defaut(&p));
}
La vraie force des énumérations Rust : chaque variante peut porter ses propres données.
enum Evenement {
Connexion { utilisateur: String, ip: String },
Deconnexion { utilisateur: String },
EchecAuthentification { ip: String, tentatives: u32 },
Redemarrage,
}
impl Evenement {
fn gravite(&self) -> &str {
match self {
Evenement::EchecAuthentification { tentatives, .. } if *tentatives >= 5 => "CRITIQUE",
Evenement::EchecAuthentification { .. } => "ALERTE",
Evenement::Redemarrage => "INFO",
_ => "INFO",
}
}
}
fn main() {
let journal = [
Evenement::Connexion { utilisateur: String::from("lina"), ip: String::from("10.0.0.12") },
Evenement::EchecAuthentification { ip: String::from("203.0.113.9"), tentatives: 7 },
Evenement::Deconnexion { utilisateur: String::from("lina") },
Evenement::Redemarrage,
];
for e in &journal {
let texte = match e {
Evenement::Connexion { utilisateur, ip } => format!("{utilisateur} connecté depuis {ip}"),
Evenement::Deconnexion { utilisateur } => format!("{utilisateur} déconnecté"),
Evenement::EchecAuthentification { ip, tentatives } => format!("{tentatives} échecs depuis {ip}"),
Evenement::Redemarrage => String::from("redémarrage du serveur"),
};
println!("[{}] {}", e.gravite(), texte);
}
}
4. Option : l’absence de valeur sans null
Beaucoup de langages ont une valeur spéciale null qui peut remplacer n’importe quel objet. Son inventeur, Tony Hoare, l’a lui-même qualifiée « d’erreur à un milliard de dollars » : oublier de tester null provoque d’innombrables plantages. Rust n’a pas de null. L’absence possible d’une valeur est dite dans le type :
enum Option<T> { // déjà définie dans la bibliothèque standard
Some(T), // il y a une valeur
None, // il n'y en a pas
}
fn diviser(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn trouver(noms: &[&str], cherche: &str) -> Option<usize> {
for (i, n) in noms.iter().enumerate() {
if *n == cherche {
return Some(i);
}
}
None
}
fn main() {
match diviser(10.0, 4.0) {
Some(r) => println!("résultat : {r}"),
None => println!("division par zéro"),
}
let classe = ["Lina", "Sam", "Noé"];
if let Some(pos) = trouver(&classe, "Sam") {
println!("Sam est en position {pos}");
}
let pos = trouver(&classe, "Zoé").unwrap_or(999);
println!("{pos}");
}
Une Option<f64> n’est pas un f64 : on ne peut pas faire de calcul avec avant d’avoir traité le cas None. Le compilateur rend l’oubli impossible.
| Méthode | Effet |
|---|---|
match / if let | Traiter explicitement les deux cas |
.unwrap_or(v) | La valeur, ou v par défaut |
.unwrap_or_default() | La valeur, ou la valeur par défaut du type (0, texte vide…) |
.is_some() / .is_none() | Tester la présence |
.map(|x| …) | Transformer la valeur si elle existe |
.unwrap() / .expect("…") | La valeur, ou arrêt du programme si None |
5. Tout ce que match sait faire
fn decrire(n: i32) -> String {
match n {
0 => String::from("zéro"),
1 | 2 | 3 => String::from("petit"), // plusieurs valeurs
4..=9 => String::from("moyen"), // intervalle
x if x < 0 => format!("négatif ({x})"), // garde
grand @ 10..=99 => format!("grand ({grand})"), // liaison + intervalle
_ => String::from("très grand"),
}
}
struct Point { x: i32, y: i32 }
fn position(p: &Point) -> &str {
match p {
Point { x: 0, y: 0 } => "origine",
Point { x: 0, .. } => "sur l'axe vertical",
Point { y: 0, .. } => "sur l'axe horizontal",
_ => "ailleurs",
}
}
fn main() {
for n in [0, 2, 7, -4, 42, 500] {
println!("{n} : {}", decrire(n));
}
println!("{}", position(&Point { x: 0, y: 5 }));
}
if let, let … else, while let
fn port(texte: &str) -> Option<u16> {
texte.parse().ok()
}
fn afficher_port(texte: &str) {
// let … else : on extrait la valeur, ou on sort de la fonction
let Some(p) = port(texte) else {
println!("« {texte} » n'est pas un port valide");
return;
};
println!("port {p}");
}
fn main() {
// if let : un seul cas nous intéresse
if let Some(p) = port("8080") {
println!("serveur sur {p}");
}
afficher_port("443");
afficher_port("99999");
// while let : on répète tant que le motif correspond
let mut pile = vec![1, 2, 3];
while let Some(sommet) = pile.pop() {
println!("dépilé : {sommet}");
}
}
6. Rendre les états invalides impossibles
Un principe de conception très puissant en Rust : si une donnée ne peut pas être invalide, on n’a jamais besoin de la vérifier. On le met en œuvre en ne permettant de créer une valeur que par un constructeur qui contrôle.
#[derive(Debug)]
pub struct Port(u16);
impl Port {
pub fn new(valeur: u16) -> Option<Port> {
if valeur == 0 { None } else { Some(Port(valeur)) }
}
pub fn valeur(&self) -> u16 {
self.0
}
pub fn est_privilegie(&self) -> bool {
self.0 < 1024
}
}
fn ouvrir_serveur(port: &Port) {
// aucune vérification nécessaire : un Port est toujours valide
println!("écoute sur le port {}", port.valeur());
}
fn main() {
match Port::new(8080) {
Some(p) => ouvrir_serveur(&p),
None => println!("port refusé"),
}
println!("{:?}", Port::new(0));
}
Dès que ce type sera placé dans un module (niveau 5), le champ privé empêchera d’écrire Port(0) directement : la seule porte d’entrée sera Port::new, qui contrôle. Le même principe s’applique à une adresse électronique, un numéro de salle, un identifiant d’utilisateur.
7. Mini-projet : le cycle de vie d’un ticket d’incident
8. Bilan
Exercices corrigés
Les corrigés privilégient les écritures idiomatiques de Rust. Si ta version fonctionne avec plus de if et moins de match, compare les deux : le match est souvent plus court, et surtout, le compilateur vérifie qu’il est complet.
Exercice 3.1 — Une structure complète
Voir le corrigé
#[derive(Debug)]
struct Eleve {
nom: String,
classe: String,
notes: [f64; 3],
}
impl Eleve {
fn new(nom: &str, classe: &str, notes: [f64; 3]) -> Self {
Self { nom: nom.to_string(), classe: classe.to_string(), notes }
}
fn moyenne(&self) -> f64 {
(self.notes[0] + self.notes[1] + self.notes[2]) / 3.0
}
fn est_admis(&self) -> bool {
self.moyenne() >= 10.0
}
fn corriger_note(&mut self, indice: usize, note: f64) -> bool {
if indice >= self.notes.len() || !(0.0..=20.0).contains(¬e) {
return false;
}
self.notes[indice] = note;
true
}
}
fn main() {
let mut a = Eleve::new("Lina", "SIO1", [12.0, 8.5, 9.0]);
let b = Eleve::new("Sam", "SIO1", [15.0, 14.0, 17.5]);
for e in [&a, &b] {
println!("{} ({}) : {:.2} admis = {}", e.nom, e.classe, e.moyenne(), e.est_admis());
}
println!("correction valide : {}", a.corriger_note(1, 14.0));
println!("note 25 : {}", a.corriger_note(0, 25.0));
println!("indice 5 : {}", a.corriger_note(5, 12.0));
println!("{} : {:.2} admis = {}", a.nom, a.moyenne(), a.est_admis());
}
Explication
Le constructeur prend des &str et crée lui-même les String : l’appel reste simple. corriger_note vérifie avant de modifier, et le bool renvoyé permet à l’appelant de savoir si sa demande a été prise en compte. Le tableau [&a, &b] contient des références : on parcourt sans déplacer les élèves.
Pour aller plus loin
Au niveau 4, corriger_note renverra plutôt un Result, capable d’expliquer pourquoi la correction a été refusée.
Exercice 3.2 — Énumérations et Option
Voir le corrigé
use std::f64::consts::PI;
#[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 } => PI * rayon * rayon,
Forme::Rectangle { largeur, hauteur } => largeur * hauteur,
Forme::Triangle { base, hauteur } => base * hauteur / 2.0,
}
}
}
fn plus_grande(formes: &[Forme]) -> Option<&Forme> {
let mut meilleure: Option<&Forme> = None;
for f in formes {
match meilleure {
Some(m) if m.aire() >= f.aire() => {}
_ => meilleure = Some(f),
}
}
meilleure
}
fn jour_suivant(jour: &str) -> Option<&str> {
let jours = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
for (i, j) in jours.iter().enumerate() {
if *j == jour {
return Some(jours[(i + 1) % 7]);
}
}
None
}
fn main() {
let formes = [
Forme::Cercle { rayon: 1.5 },
Forme::Rectangle { largeur: 3.0, hauteur: 2.0 },
Forme::Triangle { base: 4.0, hauteur: 5.0 },
Forme::Cercle { rayon: 0.5 },
];
let mut total = 0.0;
for f in &formes {
println!("{:?} : {:.2}", f, f.aire());
total += f.aire();
}
println!("Aire totale : {total:.2}");
println!("La plus grande : {:?}", plus_grande(&formes));
println!("Liste vide : {:?}", plus_grande(&[]));
let Some(demain) = jour_suivant("dimanche") else {
println!("jour inconnu");
return;
};
println!("Après dimanche vient {demain}");
}
Explication
Le match de aire est exhaustif : ajouter une variante Hexagone sans compléter la méthode provoquerait une erreur de compilation. C’est un filet de sécurité précieux quand un programme évolue. plus_grande renvoie une référence vers une forme de la liste, sans la copier.
Mini-projet
Voir le corrigé
#[derive(Debug)]
enum Etat {
Ouvert,
EnCours { technicien: String },
Resolu { technicien: String, solution: String },
Ferme,
}
#[derive(Debug)]
struct Ticket {
numero: u32,
titre: String,
salle: u16,
etat: Etat,
}
impl Ticket {
fn new(numero: u32, titre: &str, salle: u16) -> Self {
Ticket { numero, titre: titre.to_string(), salle, etat: Etat::Ouvert }
}
fn prendre_en_charge(self, technicien: &str) -> Option<Ticket> {
match self.etat {
Etat::Ouvert => Some(Ticket { etat: Etat::EnCours { technicien: technicien.to_string() }, ..self }),
_ => None,
}
}
fn resoudre(self, solution: &str) -> Option<Ticket> {
match self.etat {
Etat::EnCours { technicien } => Some(Ticket {
etat: Etat::Resolu { technicien, solution: solution.to_string() },
..self
}),
_ => None,
}
}
fn fermer(self) -> Option<Ticket> {
match self.etat {
Etat::Resolu { .. } => Some(Ticket { etat: Etat::Ferme, ..self }),
_ => None,
}
}
fn resume(&self) -> String {
let etat = match &self.etat {
Etat::Ouvert => String::from("ouvert, en attente"),
Etat::EnCours { technicien } => format!("pris en charge par {technicien}"),
Etat::Resolu { technicien, solution } => format!("résolu par {technicien} : {solution}"),
Etat::Ferme => String::from("fermé"),
};
format!("#{} [salle {}] {} — {}", self.numero, self.salle, self.titre, etat)
}
}
fn main() {
let t = Ticket::new(42, "Vidéoprojecteur sans signal", 207);
println!("{}", t.resume());
let t = t.prendre_en_charge("Magali").expect("transition autorisée");
println!("{}", t.resume());
let t = t.resoudre("câble HDMI remplacé").expect("transition autorisée");
println!("{}", t.resume());
let t = t.fermer().expect("transition autorisée");
println!("{}", t.resume());
let autre = Ticket::new(43, "Imprimante bloquée", 112);
match autre.resoudre("?") {
Some(_) => println!("anomalie : transition acceptée"),
None => println!("refusé : on ne résout pas un ticket qui n'est pas en cours"),
}
}
Explication
Les méthodes de transition prennent self : l’ancien ticket est consommé, il ne peut plus être utilisé par erreur. La syntaxe ..self recopie les champs inchangés. Les données portées par l’état (le technicien) sont transmises d’un état à l’autre par le match, ce qui garantit qu’un ticket résolu a toujours un technicien.
Pour aller plus loin
Une variante très utilisée de ce motif consiste à faire de chaque état un type différent (TicketOuvert, TicketEnCours…) : une transition interdite devient alors une erreur de compilation.
🧠 Testez vos connaissances
5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.