1. Les traits
Un trait décrit un comportement : une liste de méthodes qu’un type s’engage à fournir.
trait Decrire {
fn description(&self) -> String;
// méthode avec une implémentation par défaut
fn fiche(&self) -> String {
format!("[fiche] {}", self.description())
}
}
struct Poste { nom: String, ram_go: u32 }
struct Imprimante { modele: String, couleur: bool }
impl Decrire for Poste {
fn description(&self) -> String {
format!("poste {} ({} Go)", self.nom, self.ram_go)
}
}
impl Decrire for Imprimante {
fn description(&self) -> String {
let c = if self.couleur { "couleur" } else { "noir et blanc" };
format!("imprimante {} {}", self.modele, c)
}
fn fiche(&self) -> String { // on remplace la version par défaut
format!("[matériel partagé] {}", self.description())
}
}
fn main() {
let p = Poste { nom: String::from("PC-207-01"), ram_go: 16 };
let i = Imprimante { modele: String::from("LaserPro"), couleur: false };
println!("{}", p.fiche());
println!("{}", i.fiche());
}
Les traits standard et derive
| Trait | Apporte | Obtention |
|---|---|---|
Debug | Affichage {:?} | #[derive(Debug)] |
Display | Affichage {} et .to_string() | À écrire soi-même |
Clone, Copy | Copie explicite, copie implicite | #[derive(Clone, Copy)] |
PartialEq, Eq | Comparaison == | #[derive(PartialEq, Eq)] |
PartialOrd, Ord | Comparaisons <, > et tri | #[derive(PartialOrd, Ord)] |
Hash | Utilisation comme clé de HashMap | #[derive(Hash)] |
Default | Valeur par défaut Type::default() | #[derive(Default)] |
From<T> | Conversion Type::from(t) et t.into() | À écrire soi-même |
use std::fmt;
#[derive(Debug, Clone, PartialEq, Default)]
struct Salle {
batiment: char,
numero: u16,
}
impl fmt::Display for Salle {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{:03}", self.batiment, self.numero)
}
}
impl From<(char, u16)> for Salle {
fn from((batiment, numero): (char, u16)) -> Self {
Salle { batiment, numero }
}
}
fn main() {
let a = Salle { batiment: 'B', numero: 7 };
let b: Salle = ('B', 7).into(); // grâce à From
println!("{a} / {:?} / égales : {}", b, a == b);
println!("par défaut : {:?}", Salle::default());
}
2. Les génériques
Une fonction générique fonctionne pour plusieurs types. On écrit le type sous forme de paramètre T, et on précise ce que T doit savoir faire : c’est une contrainte de trait.
// T doit pouvoir être comparé (PartialOrd)
fn plus_grand<T: PartialOrd>(valeurs: &[T]) -> Option<&T> {
let mut max = valeurs.first()?;
for v in valeurs {
if v > max {
max = v;
}
}
Some(max)
}
fn main() {
println!("{:?}", plus_grand(&[3, 9, 2]));
println!("{:?}", plus_grand(&[1.5, 0.2]));
println!("{:?}", plus_grand(&["poire", "abricot", "sureau"]));
println!("{:?}", plus_grand::<i32>(&[]));
}
Le compilateur génère une version de la fonction pour chaque type utilisé : les génériques ne coûtent rien à l’exécution.
use std::fmt::Display;
// plusieurs contraintes, écrites avec where pour la lisibilité
fn afficher_tous<T>(elements: &[T])
where
T: Display,
{
for (i, e) in elements.iter().enumerate() {
println!("{}. {e}", i + 1);
}
}
// une structure générique
#[derive(Debug)]
struct Paire<T> {
a: T,
b: T,
}
impl<T: PartialOrd + Copy> Paire<T> {
fn plus_grand(&self) -> T {
if self.a >= self.b { self.a } else { self.b }
}
}
fn main() {
afficher_tous(&["Lina", "Sam"]);
afficher_tous(&[3.5, 7.25]);
let p = Paire { a: 4, b: 11 };
println!("{:?} → {}", p, p.plus_grand());
}
impl Trait et dyn Trait
trait Forme {
fn aire(&self) -> f64;
fn nom(&self) -> String;
}
struct Cercle { r: f64 }
struct Carre { c: f64 }
impl Forme for Cercle {
fn aire(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
fn nom(&self) -> String { format!("cercle de rayon {}", self.r) }
}
impl Forme for Carre {
fn aire(&self) -> f64 { self.c * self.c }
fn nom(&self) -> String { format!("carré de côté {}", self.c) }
}
// impl Forme : « n'importe quel type qui implémente Forme » (un seul type à la fois)
fn decrire(f: &impl Forme) {
println!("{} : {:.2}", f.nom(), f.aire());
}
fn main() {
decrire(&Cercle { r: 1.0 });
// dyn Forme : une collection de types DIFFÉRENTS qui implémentent Forme
let formes: Vec<Box<dyn Forme>> = vec![
Box::new(Cercle { r: 2.0 }),
Box::new(Carre { c: 3.0 }),
];
let total: f64 = formes.iter().map(|f| f.aire()).sum();
println!("aire totale : {total:.2}");
}
| Écriture | Sens | Usage |
|---|---|---|
fn f<T: Forme>(x: &T) ou fn f(x: &impl Forme) | Un type précis, choisi à la compilation | Cas général, le plus rapide |
Vec<Box<dyn Forme>> | Des types différents, choisis à l’exécution | Collections hétérogènes, greffons |
3. Fermetures et itérateurs
Une fermeture (closure) est une fonction anonyme, écrite entre barres verticales, qui peut utiliser les variables qui l’entourent.
fn main() {
let seuil = 10.0;
let est_admis = |note: f64| note >= seuil; // capture seuil
let double = |x| x * 2;
println!("{} {}", est_admis(12.5), double(21));
}
Un itérateur produit des éléments un par un. On enchaîne des adaptateurs qui transforment le flux, puis un consommateur qui produit le résultat. Rien n’est calculé tant qu’il n’y a pas de consommateur.
fn main() {
let notes = vec![12.0, 7.5, 15.0, 9.0, 18.5, 10.0];
let admises: Vec<f64> = notes.iter().copied().filter(|n| *n >= 10.0).collect();
let bonifiees: Vec<f64> = notes.iter().map(|n| (n + 1.0).min(20.0)).collect();
let nb_echecs = notes.iter().filter(|n| **n < 10.0).count();
let somme: f64 = notes.iter().sum();
let meilleure = notes.iter().copied().fold(f64::MIN, f64::max);
let tous_positifs = notes.iter().all(|n| *n >= 0.0);
let un_vingt = notes.iter().any(|n| *n == 20.0);
let position = notes.iter().position(|n| *n == 15.0);
println!("{admises:?}\n{bonifiees:?}");
println!("{nb_echecs} échecs, somme {somme}, meilleure {meilleure}");
println!("{tous_positifs} {un_vingt} {position:?}");
let noms = ["Lina", "Sam", "Noé"];
for (rang, (nom, note)) in noms.iter().zip(notes.iter()).enumerate() {
println!("{}. {nom} : {note}", rang + 1);
}
}
| Adaptateur ou consommateur | Effet |
|---|---|
map(|x| …) | Transforme chaque élément |
filter(|x| …) | Garde les éléments qui vérifient la condition |
enumerate(), zip(autre) | Ajoute l’indice / avance sur deux itérateurs à la fois |
take(n), skip(n) | Les n premiers / tout sauf les n premiers |
collect() | Rassemble dans une collection (Vec, String, HashMap…) |
sum(), count(), min(), max() | Calcule un résultat |
any(), all(), find(), position() | Recherche, s’arrête dès que possible |
fold(init, |acc, x| …) | Accumule un résultat quelconque |
4. Les durées de vie
Quand une fonction renvoie une référence, le compilateur doit savoir de quel paramètre elle provient, pour vérifier qu’elle ne survivra pas à la donnée. Le plus souvent, il le déduit seul. Sinon, on l’indique avec une durée de vie notée 'a :
fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
// une structure qui contient une référence doit aussi l'annoncer
struct Extrait<'a> {
texte: &'a str,
}
fn main() {
let s1 = String::from("administration");
let resultat;
{
let s2 = String::from("réseau");
resultat = plus_long(&s1, &s2);
println!("{resultat}");
}
// println!("{resultat}"); ← refusé : s2 n'existe plus, resultat pourrait la désigner
let e = Extrait { texte: &s1[0..5] };
println!("{}", e.texte);
}
'a ne change pas la durée de vie des données : il décrit une relation (« le résultat vit au plus aussi longtemps que a et b ») que le compilateur vérifie. Si les durées de vie deviennent compliquées dans ton code, c’est souvent le signe qu’une structure devrait posséder ses données (String) plutôt que les emprunter (&str).
5. Organiser le code en modules
mod validation {
pub struct Identifiant(String); // la structure est publique…
impl Identifiant {
pub fn new(texte: &str) -> Option<Self> {
let ok = (3..=20).contains(&texte.len())
&& texte.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit());
if ok { Some(Identifiant(texte.to_string())) } else { None }
}
pub fn valeur(&self) -> &str {
&self.0 // …mais son champ est privé
}
}
}
use validation::Identifiant;
fn main() {
let id = Identifiant::new("ljousset").expect("identifiant valide");
println!("{}", id.valeur());
println!("{}", Identifiant::new("Admin!").is_none());
// let pirate = Identifiant(String::from("Admin!")); ← refusé : champ privé
}
- Tout est privé par défaut :
pubrend un élément visible hors de son module. - Un champ privé force le passage par le constructeur : c’est ainsi que l’on garantit les types valides du niveau 3.
- Dans un projet, chaque module va dans son fichier :
mod validation;dansmain.rschargesrc/validation.rs. - Un projet peut contenir une bibliothèque (
src/lib.rs) et un exécutable (src/main.rs) qui l’utilise : c’est l’organisation du niveau 6.
6. Les tests automatisés
Rust intègre les tests au langage. On les place dans un module tests, compilé seulement lors de cargo test.
pub fn moyenne(notes: &[f64]) -> Option<f64> {
if notes.is_empty() {
return None;
}
Some(notes.iter().sum::<f64>() / notes.len() as f64)
}
pub fn mention(moyenne: f64) -> &'static str {
match moyenne {
m if m >= 16.0 => "très bien",
m if m >= 14.0 => "bien",
m if m >= 12.0 => "assez bien",
m if m >= 10.0 => "passable",
_ => "non admis",
}
}
#[cfg(test)]
mod tests {
use super::*; // accès aux fonctions du fichier
#[test]
fn moyenne_simple() {
assert_eq!(moyenne(&[10.0, 20.0]), Some(15.0));
}
#[test]
fn moyenne_liste_vide() {
assert_eq!(moyenne(&[]), None);
}
#[test]
fn mentions_aux_limites() {
assert_eq!(mention(16.0), "très bien");
assert_eq!(mention(15.99), "bien");
assert_eq!(mention(10.0), "passable");
assert_eq!(mention(9.99), "non admis");
}
#[test]
#[should_panic]
fn indice_invalide() {
let v: Vec<i32> = Vec::new();
let _ = v[0]; // ce test réussit si le code panique
}
}
fn main() {
println!("{:?} {}", moyenne(&[12.0, 14.0]), mention(13.0));
}
$ cargo test
running 4 tests
test tests::indice_invalide - should panic ... ok
test tests::mentions_aux_limites ... ok
test tests::moyenne_liste_vide ... ok
test tests::moyenne_simple ... ok
test result: ok. 4 passed; 0 failed
| Macro | Vérifie |
|---|---|
assert!(condition) | Que la condition est vraie |
assert_eq!(a, b) | Que a == b, et affiche les deux valeurs sinon |
assert_ne!(a, b) | Que a != b |
#[should_panic] | Que le test provoque un arrêt |
7. Mini-projet : le contrôleur de mots de passe
8. Bilan
Exercices corrigés
Les corrigés de ce niveau contiennent leurs propres tests. Lance cargo test sur ton code et sur le corrigé : si un test du corrigé échoue avec ta version, il a trouvé un cas que tu n’avais pas prévu.
Exercice 5.1 — Un trait pour le parc
Voir le corrigé
use std::fmt;
trait Equipement {
fn nom(&self) -> String;
fn consommation_watts(&self) -> u32;
fn cout_annuel(&self, heures_par_jour: u32, prix_kwh: f64) -> f64 {
let kwh = self.consommation_watts() as f64 * heures_par_jour as f64 * 365.0 / 1000.0;
kwh * prix_kwh
}
}
struct Poste { nom: String }
struct Ecran { pouces: u32 }
struct Switch { ports: u32 }
impl Equipement for Poste {
fn nom(&self) -> String { format!("poste {}", self.nom) }
fn consommation_watts(&self) -> u32 { 65 }
}
impl Equipement for Ecran {
fn nom(&self) -> String { format!("écran {}\"", self.pouces) }
fn consommation_watts(&self) -> u32 { if self.pouces > 24 { 30 } else { 20 } }
}
impl Equipement for Switch {
fn nom(&self) -> String { format!("switch {} ports", self.ports) }
fn consommation_watts(&self) -> u32 { 5 + self.ports }
}
impl fmt::Display for Poste {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Poste<{}>", self.nom)
}
}
fn inventaire<T: fmt::Display>(liste: &[T]) {
for e in liste {
println!("- {e}");
}
}
fn main() {
let salle: Vec<Box<dyn Equipement>> = vec![
Box::new(Poste { nom: String::from("PC-01") }),
Box::new(Poste { nom: String::from("PC-02") }),
Box::new(Ecran { pouces: 27 }),
Box::new(Ecran { pouces: 22 }),
Box::new(Switch { ports: 24 }),
];
let mut total = 0.0;
for e in &salle {
let c = e.cout_annuel(8, 0.25);
total += c;
println!("{:<18} {:>4} W {:>8.2} €", e.nom(), e.consommation_watts(), c);
}
println!("{:<25} {:>8.2} €", "Total", total);
inventaire(&[Poste { nom: String::from("PC-01") }, Poste { nom: String::from("PC-02") }]);
}
Explication
La méthode par défaut cout_annuel s’appuie sur consommation_watts, que chaque type fournit : le calcul n’existe qu’en un seul exemplaire. Vec<Box<dyn Equipement>> range dans une même liste des types différents ; inventaire, générique, accepte une liste de n’importe quel type affichable.
Exercice 5.2 — Sans boucle for
Voir le corrigé
fn main() {
let mots = vec!["serveur", "Pare-feu", "routeur", "", "switch", "VLAN", "proxy"];
let non_vides = mots.iter().filter(|m| !m.is_empty()).count();
let mut minuscules: Vec<String> = mots
.iter()
.filter(|m| !m.is_empty())
.map(|m| m.to_lowercase())
.collect();
minuscules.sort();
let plus_long = mots.iter().max_by_key(|m| m.chars().count());
let total: usize = mots
.iter()
.map(|m| m.chars().count())
.filter(|&n| n > 5)
.sum();
let initiales: String = mots
.iter()
.filter_map(|m| m.chars().next())
.map(|c| c.to_ascii_uppercase())
.collect();
println!("{non_vides}\n{minuscules:?}\n{plus_long:?}\n{total}\n{initiales}");
}
Explication
filter_map combine un filtre et une transformation : chars().next() renvoie None pour le mot vide, qui est ainsi éliminé. collect() sait fabriquer une String à partir de caractères. max_by_key renvoie le dernier maximum en cas d’égalité : « serveur », « routeur » et « Pare-feu » ont tous 7 ou 8 caractères, vérifie lequel sort.
Exercice 5.3 — Tester une fonction existante
Voir le corrigé
fn sans_accent(c: char) -> char {
match c {
'à' | 'â' | 'ä' => 'a',
'é' | 'è' | 'ê' | 'ë' => 'e',
'î' | 'ï' => 'i',
'ô' | 'ö' => 'o',
'ù' | 'û' | 'ü' => 'u',
'ç' => 'c',
'ÿ' => 'y',
autre => autre,
}
}
pub fn identifiant(prenom: &str, nom: &str) -> Option<String> {
let initiale = prenom.trim().chars().next()?;
let brut: String = std::iter::once(initiale)
.chain(nom.chars())
.flat_map(|c| c.to_lowercase())
.map(sans_accent)
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
.take(20)
.collect();
if brut.is_empty() { None } else { Some(brut) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn cas_simple() { assert_eq!(identifiant("Lina", "Martin"), Some("lmartin".into())); }
#[test] fn accents() { assert_eq!(identifiant("Élodie", "Brûlé"), Some("ebrule".into())); }
#[test] fn nom_compose() { assert_eq!(identifiant("Jean", "Le Goff-Durand"), Some("jlegoffdurand".into())); }
#[test] fn apostrophe() { assert_eq!(identifiant("Sean", "O'Neil"), Some("soneil".into())); }
#[test] fn cedille() { assert_eq!(identifiant("François", "Garçon"), Some("fgarcon".into())); }
#[test] fn troncature() { assert_eq!(identifiant("A", "Abcdefghijklmnopqrstuvwxyz").map(|s| s.len()), Some(20)); }
#[test] fn prenom_vide() { assert_eq!(identifiant("", "Martin"), None); }
#[test] fn que_des_symboles() { assert_eq!(identifiant("-", "!!"), None); }
#[test] fn espaces_autour() { assert_eq!(identifiant(" Sam ", " Noël"), Some("snoel".into())); }
}
fn main() {
println!("{:?}", identifiant("Élodie", "Le Brûlé"));
}
Explication
std::iter::once(initiale).chain(nom.chars()) construit un flux « initiale puis nom » sans créer de texte intermédiaire. to_lowercase() renvoie un itérateur (certains caractères deviennent plusieurs lettres), d’où flat_map. Le filtre final ne garde que les lettres ASCII minuscules et les chiffres : c’est une liste blanche, plus sûre qu’une liste de caractères interdits.
Pour aller plus loin
La bibliothèque unicode-normalization gère tous les accents de façon exhaustive ; notre match couvre le français courant.
Mini-projet
Voir le corrigé
mod politique {
pub trait Regle {
fn verifier(&self, mdp: &str) -> Result<(), String>;
}
pub struct LongueurMin(pub usize);
pub struct Majuscule;
pub struct Minuscule;
pub struct Chiffre;
pub struct Special;
pub struct PasDansListe(pub Vec<&'static str>);
impl Regle for LongueurMin {
fn verifier(&self, mdp: &str) -> Result<(), String> {
let n = mdp.chars().count();
if n >= self.0 { Ok(()) } else { Err(format!("au moins {} caractères ({n} actuellement)", self.0)) }
}
}
impl Regle for Majuscule {
fn verifier(&self, mdp: &str) -> Result<(), String> {
if mdp.chars().any(|c| c.is_uppercase()) { Ok(()) } else { Err("une majuscule".into()) }
}
}
impl Regle for Minuscule {
fn verifier(&self, mdp: &str) -> Result<(), String> {
if mdp.chars().any(|c| c.is_lowercase()) { Ok(()) } else { Err("une minuscule".into()) }
}
}
impl Regle for Chiffre {
fn verifier(&self, mdp: &str) -> Result<(), String> {
if mdp.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err("un chiffre".into()) }
}
}
impl Regle for Special {
fn verifier(&self, mdp: &str) -> Result<(), String> {
if mdp.chars().any(|c| !c.is_alphanumeric() && !c.is_whitespace()) {
Ok(())
} else {
Err("un caractère spécial".into())
}
}
}
impl Regle for PasDansListe {
fn verifier(&self, mdp: &str) -> Result<(), String> {
let m = mdp.to_lowercase();
if self.0.iter().any(|interdit| *interdit == m) {
Err("mot de passe trop courant".into())
} else {
Ok(())
}
}
}
pub struct Politique {
regles: Vec<Box<dyn Regle>>,
}
impl Politique {
pub fn new() -> Self { Politique { regles: Vec::new() } }
pub fn ajouter(mut self, r: impl Regle + 'static) -> Self {
self.regles.push(Box::new(r));
self
}
pub fn standard() -> Self {
Politique::new()
.ajouter(LongueurMin(12))
.ajouter(Majuscule)
.ajouter(Minuscule)
.ajouter(Chiffre)
.ajouter(Special)
.ajouter(PasDansListe(vec!["azerty", "motdepasse", "123456", "password", "azertyuiop123!"]))
}
pub fn controler(&self, mdp: &str) -> Vec<String> {
self.regles.iter().filter_map(|r| r.verifier(mdp).err()).collect()
}
}
}
use politique::Politique;
fn main() {
let p = Politique::standard();
for mdp in ["azerty", "Soleil-2026-Angers", "tropcourt1!A"] {
let erreurs = p.controler(mdp);
if erreurs.is_empty() {
println!("{mdp:<22} accepté");
} else {
println!("{mdp:<22} refusé : {}", erreurs.join(", "));
}
}
}
#[cfg(test)]
mod tests {
use super::politique::*;
fn p() -> Politique { Politique::standard() }
#[test] fn valide() { assert!(p().controler("Soleil-2026-Angers").is_empty()); }
#[test] fn trop_court() { assert_eq!(p().controler("Ab1!efghijk").len(), 1); }
#[test] fn sans_majuscule() { assert_eq!(p().controler("soleil-2026-angers"), vec!["une majuscule"]); }
#[test] fn sans_minuscule() { assert_eq!(p().controler("SOLEIL-2026-ANGERS"), vec!["une minuscule"]); }
#[test] fn sans_chiffre() { assert_eq!(p().controler("Soleil-deux-Angers"), vec!["un chiffre"]); }
#[test] fn sans_special() { assert_eq!(p().controler("Soleil2026Angers"), vec!["un caractère spécial"]); }
#[test] fn courant_casse_ignoree() {
assert!(p().controler("AZERTYUIOP123!").contains(&"mot de passe trop courant".to_string()));
}
#[test] fn vide() { assert!(p().controler("").len() >= 5); }
#[test] fn espaces_ne_sont_pas_speciaux() { assert!(p().controler("Soleil 2026 Angers").contains(&"un caractère spécial".to_string())); }
#[test] fn unicode_compte_en_caracteres() { assert!(p().controler("Éléphant-2026").is_empty()); }
#[test] fn politique_sur_mesure() {
let courte = Politique::new().ajouter(LongueurMin(4));
assert!(courte.controler("abcd").is_empty());
}
}
Explication
Chaque règle est un petit type indépendant ; Politique ne connaît que le trait Regle. Ajouter une règle, c’est ajouter un type, sans toucher au reste : c’est le principe ouvert-fermé. La méthode ajouter prend et renvoie self, ce qui permet d’enchaîner les appels. filter_map(|r| r.verifier(mdp).err()) collecte seulement les erreurs.
Pour aller plus loin
En production, la liste des mots de passe courants contient des centaines de milliers d’entrées, issues de fuites réelles. On la charge depuis un fichier dans un HashSet pour une recherche instantanée.
🧠 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.