1. Deux familles d’erreurs
| panic! | Result<T, E> | |
|---|---|---|
| Nature | Bug, situation impossible | Échec prévisible : fichier absent, saisie invalide, réseau coupé |
| Effet | Arrêt du programme (du fil d’exécution) | Une valeur que l’appelant doit traiter |
| Exemples | Indice hors limites, unwrap() sur None | "abc".parse::<i32>(), File::open(…) |
enum Result<T, E> { // déjà définie dans la bibliothèque standard
Ok(T), // succès, avec la valeur produite
Err(E), // échec, avec la description de l'erreur
}
fn main() {
let essais = ["42", "-7", "abc", ""];
for texte in essais {
match texte.parse::<i32>() {
Ok(n) => println!("« {texte} » → {n}"),
Err(e) => println!("« {texte} » → erreur : {e}"),
}
}
}
Comme Option, un Result oblige l’appelant à regarder ce qui s’est passé : impossible d’utiliser la valeur sans avoir traité l’erreur. C’est la grande différence avec les exceptions, qui peuvent être oubliées.
2. Renvoyer et propager des erreurs
fn lire_age(texte: &str) -> Result<u8, String> {
let age: u8 = match texte.trim().parse() {
Ok(a) => a,
Err(_) => return Err(format!("« {texte} » n'est pas un nombre entre 0 et 255")),
};
if !(15..=99).contains(&age) {
return Err(format!("âge {age} hors de la plage 15 à 99"));
}
Ok(age)
}
fn main() {
for t in ["19", "7", "abc", "300"] {
match lire_age(t) {
Ok(a) => println!("âge accepté : {a}"),
Err(msg) => println!("refusé : {msg}"),
}
}
}
L’opérateur ?
Écrire un match à chaque appel devient vite lourd. L’opérateur ? fait la même chose en un caractère : si c’est Ok, prends la valeur ; si c’est Err, renvoie immédiatement l’erreur à l’appelant.
use std::num::ParseIntError;
fn somme(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.trim().parse()?;
let y: i32 = b.trim().parse()?;
Ok(x + y)
}
fn main() {
println!("{:?}", somme("12", "30")); // Ok(42)
println!("{:?}", somme("12", "trente"));// Err(ParseIntError { kind: InvalidDigit })
}
? ne s’utilise que dans une fonction qui renvoie elle-même un Result (ou un Option, pour les Option). Même main peut renvoyer un Result :
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let n: i32 = "42".parse()?;
println!("{n}");
Ok(())
}
Box<dyn Error> signifie « n’importe quel type d’erreur » : pratique dans main et dans les petits programmes.
| Écriture | Quand l’utiliser |
|---|---|
match sur le résultat | Pour traiter chaque cas différemment |
? | Pour laisser l’appelant décider |
.unwrap_or(v) / .unwrap_or_else(|e| …) | Pour une valeur de repli |
.map_err(|e| …) | Pour transformer l’erreur en une autre, plus parlante |
.ok() | Pour convertir en Option en ignorant la cause |
.expect("…") | Seulement si l’échec est un bug : arrête le programme |
3. Ses propres types d’erreur
Un texte d’erreur suffit pour un exemple. Dans une vraie application, l’appelant a besoin de distinguer les erreurs pour réagir différemment : on les décrit par une énumération.
use std::fmt;
#[derive(Debug, PartialEq)]
enum ErreurNote {
Vide,
PasUnNombre(String),
HorsBornes(f64),
}
impl fmt::Display for ErreurNote {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErreurNote::Vide => write!(f, "note manquante"),
ErreurNote::PasUnNombre(t) => write!(f, "« {t} » n'est pas une note"),
ErreurNote::HorsBornes(n) => write!(f, "{n} n'est pas entre 0 et 20"),
}
}
}
impl std::error::Error for ErreurNote {}
fn lire_note(texte: &str) -> Result<f64, ErreurNote> {
let t = texte.trim().replace(',', "."); // accepte 12,5 comme 12.5
if t.is_empty() {
return Err(ErreurNote::Vide);
}
let n: f64 = t.parse().map_err(|_| ErreurNote::PasUnNombre(texte.to_string()))?;
if !(0.0..=20.0).contains(&n) {
return Err(ErreurNote::HorsBornes(n));
}
Ok(n)
}
fn main() {
for t in ["14", "12,5", "", "vingt", "21"] {
match lire_note(t) {
Ok(n) => println!("{n}"),
Err(e) => println!("erreur : {e}"),
}
}
assert_eq!(lire_note("25"), Err(ErreurNote::HorsBornes(25.0)));
}
- Implémenter
Displayfournit le message destiné à l’utilisateur ({}). - Implémenter
std::error::Errorrend le type compatible avecBox<dyn Error>et avec l’écosystème. - Les traits, dont
Displayest un exemple, sont l’objet du niveau 5 : pour l’instant, recopie ce modèle.
4. Les vecteurs : Vec<T>
Un vecteur est une liste de valeurs du même type, qui peut grandir.
fn main() {
let mut notes: Vec<f64> = Vec::new();
notes.push(12.0);
notes.push(15.5);
notes.push(9.0);
let autres = vec![17.0, 11.0]; // macro de création
notes.extend(&autres);
println!("{} notes : {:?}", notes.len(), notes);
println!("première : {}", notes[0]); // arrêt si l'indice n'existe pas
println!("dixième : {:?}", notes.get(9)); // None : accès sûr
notes.sort_by(|a, b| a.partial_cmp(b).unwrap());
println!("triées : {:?}", notes);
notes.retain(|n| *n >= 10.0); // garde seulement ce qui vérifie la condition
println!("≥ 10 : {:?}", notes);
if let Some(derniere) = notes.pop() { // retire et renvoie le dernier élément
println!("retirée : {derniere}");
}
for n in ¬es { // parcours sans déplacer le vecteur
print!("{n} ");
}
println!();
for n in notes.iter_mut() { // parcours avec modification
*n += 1.0;
}
println!("bonus : {:?}", notes);
}
| Méthode | Effet |
|---|---|
push(x) / pop() | Ajouter à la fin / retirer le dernier (renvoie Option) |
v[i] / v.get(i) | Lire (arrêt si hors limites) / lire en Option |
len(), is_empty() | Taille, test de vide |
contains(&x) | Présence d’une valeur |
sort(), sort_by(…), dedup() | Trier, retirer les doublons consécutifs |
retain(|x| …) | Ne garder que les éléments qui vérifient une condition |
insert(i, x), remove(i) | Insérer ou retirer à une position |
5. Les textes, en détail
fn main() {
let mut s = String::from("Lycée");
s.push_str(" Chevrollier");
let complet = format!("{s} - Angers"); // format! ne déplace rien
let concat = s.clone() + " (49)"; // + prend possession du premier texte
println!("{complet} / {concat}");
println!("octets : {}, caractères : {}", s.len(), s.chars().count());
println!("{}", s.to_uppercase());
println!("{:?}", s.find("Chev")); // Some(7) : position en octets
println!("{}", s.replace("Lycée", "LP"));
println!("{}", " espaces ".trim());
println!("{:?}", "a;b;;c".split(';').collect::<Vec<&str>>());
println!("{}", s.starts_with("Ly"));
for (i, c) in "été".char_indices() {
println!("octet {i} : {c}");
}
}
6. Les tables : HashMap<K, V>
Une table de hachage associe des clés uniques à des valeurs.
use std::collections::HashMap;
fn main() {
let mut stock: HashMap<String, u32> = HashMap::new();
stock.insert(String::from("clavier"), 12);
stock.insert(String::from("souris"), 30);
stock.insert(String::from("clavier"), 15); // remplace l'ancienne valeur
match stock.get("écran") {
Some(q) => println!("écrans : {q}"),
None => println!("aucun écran référencé"),
}
// entry : insérer si absent, puis modifier
*stock.entry(String::from("écran")).or_insert(0) += 4;
*stock.entry(String::from("souris")).or_insert(0) -= 5;
// une HashMap n'a pas d'ordre : on trie pour afficher
let mut articles: Vec<_> = stock.iter().collect();
articles.sort();
for (article, quantite) in articles {
println!("{article:<10} {quantite:>4}");
}
println!("{} références", stock.len());
}
Le motif *table.entry(clé).or_insert(0) += 1 est le moyen de compter des occurrences : mots d’un texte, adresses IP d’un journal, notes par élève.
7. Lire et écrire des fichiers
use std::fs;
use std::io::{self, BufRead, BufReader, Write};
fn main() -> io::Result<()> {
// écrire tout un fichier
fs::write("notes.txt", "Lina;12\nSam;15\nNoé;9\n")?;
// lire tout un fichier (petits fichiers)
let contenu = fs::read_to_string("notes.txt")?;
println!("{} lignes", contenu.lines().count());
// lire ligne par ligne (gros fichiers : la mémoire reste faible)
let fichier = fs::File::open("notes.txt")?;
for (numero, ligne) in BufReader::new(fichier).lines().enumerate() {
let ligne = ligne?;
println!("{:>3} | {ligne}", numero + 1);
}
// ajouter à la fin
let mut f = fs::OpenOptions::new().append(true).open("notes.txt")?;
writeln!(f, "Zoé;17")?;
Ok(())
}
Un fichier peut ne pas exister, être protégé, ou disparaître pendant la lecture : chaque opération renvoie un Result, propagé ici par ?.
8. Mini-projet : le bulletin de notes
9. Bilan
Exercices corrigés
La question à se poser devant chaque ligne de ces corrigés : que se passe-t-il si la donnée est fausse ? Un bon traitement d’erreurs se reconnaît à ce qu’aucune entrée, même absurde, ne fait planter le programme.
Exercice 4.1 — Valider une adresse IPv4
Voir le corrigé
use std::fmt;
#[derive(Debug, PartialEq)]
enum ErreurIp {
MauvaisNombreDeParties(usize),
OctetInvalide(String),
}
impl fmt::Display for ErreurIp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErreurIp::MauvaisNombreDeParties(n) => write!(f, "4 nombres attendus, {n} trouvé(s)"),
ErreurIp::OctetInvalide(t) => write!(f, "« {t} » n'est pas un nombre de 0 à 255"),
}
}
}
fn lire_ipv4(texte: &str) -> Result<[u8; 4], ErreurIp> {
let parties: Vec<&str> = texte.trim().split('.').collect();
if parties.len() != 4 {
return Err(ErreurIp::MauvaisNombreDeParties(parties.len()));
}
let mut octets = [0u8; 4];
for (i, p) in parties.iter().enumerate() {
octets[i] = p.parse().map_err(|_| ErreurIp::OctetInvalide(p.to_string()))?;
}
Ok(octets)
}
fn main() {
for t in ["192.168.1.10", "10.0.0.1", "192.168.1", "192.168.1.300", "192.168.a.1", "", "1.2.3.4.5", "01.2.3.4"] {
match lire_ipv4(t) {
Ok(ip) => println!("{t:<16} → {:?}", ip),
Err(e) => println!("{t:<16} → erreur : {e}"),
}
}
assert_eq!(lire_ipv4("192.168.1"), Err(ErreurIp::MauvaisNombreDeParties(3)));
}
Explication
Le type u8 fait la moitié du travail : parse::<u8>() refuse d’office les négatifs, les nombres supérieurs à 255 et le texte. La chaîne vide produit une seule partie vide, donc l’erreur MauvaisNombreDeParties(1).
Pour aller plus loin
La bibliothèque standard fournit std::net::Ipv4Addr, qui fait exactement ce travail : "192.168.1.10".parse::<Ipv4Addr>(). Dans un vrai projet, on l’utilise plutôt que d’écrire son propre analyseur.
Exercice 4.2 — Statistiques
Voir le corrigé
fn statistiques(valeurs: &[f64]) -> Option<(f64, f64, f64, f64)> {
if valeurs.is_empty() {
return None;
}
let mut tri = valeurs.to_vec();
tri.sort_by(|a, b| a.total_cmp(b));
let min = tri[0];
let max = tri[tri.len() - 1];
let moyenne = tri.iter().sum::<f64>() / tri.len() as f64;
let milieu = tri.len() / 2;
let mediane = if tri.len() % 2 == 0 {
(tri[milieu - 1] + tri[milieu]) / 2.0
} else {
tri[milieu]
};
Some((min, max, moyenne, mediane))
}
fn main() {
println!("{:?}", statistiques(&[12.0, 15.5, 9.0, 17.0]));
println!("{:?}", statistiques(&[12.0, 15.5, 9.0]));
println!("{:?}", statistiques(&[1.0, 2.0, 3.0, 4.0]));
println!("{:?}", statistiques(&[]));
if let Some((min, max, moy, med)) = statistiques(&[8.0, 14.0, 11.0]) {
println!("min {min} max {max} moyenne {moy:.2} médiane {med}");
}
}
Explication
Le test de liste vide en tout début de fonction garantit que tri[0] existe ensuite. total_cmp trie des f64 sans unwrap(). tri.len() as f64 est une conversion sans risque, d’entier vers décimal.
Exercice 4.3 — Fréquence des mots
Voir le corrigé
use std::collections::HashMap;
fn frequences(texte: &str) -> HashMap<String, usize> {
let mut compte = HashMap::new();
for mot in texte.split_whitespace() {
let propre: String = mot
.chars()
.filter(|c| !".,;:!?()\"'".contains(*c))
.collect::<String>()
.to_lowercase();
if !propre.is_empty() {
*compte.entry(propre).or_insert(0) += 1;
}
}
compte
}
fn main() {
let texte = "Rust est rapide. Rust est sûr ! Le compilateur de Rust est exigeant, \
mais le compilateur est utile : il est un allié.";
let compte = frequences(texte);
let mut classement: Vec<(&String, &usize)> = compte.iter().collect();
classement.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
for (mot, n) in classement.iter().take(5) {
println!("{mot:<12} {n}");
}
}
Explication
Le tri combine deux critères : b.1.cmp(a.1) classe par nombre décroissant (b avant a), puis .then(a.0.cmp(b.0)) départage par ordre alphabétique. take(5) ne garde que les cinq premiers. Le filtre de ponctuation est un itérateur : le niveau 5 les présente en détail.
Mini-projet
Voir le corrigé
use std::collections::HashMap;
use std::fmt;
const EXPORT: &str = "Nom;Matière;Note
Lina;Maths;14
Lina;Réseau;16,5
Sam;Maths;9
Sam;Réseau;abc
Noé;Maths;12
Noé;Réseau;
Zoé;Maths;21
Lina;Anglais;11
ligne sans séparateur
Sam;Anglais;13";
#[derive(Debug)]
enum ErreurNote {
Vide,
PasUnNombre(String),
HorsBornes(f64),
}
impl fmt::Display for ErreurNote {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ErreurNote::Vide => write!(f, "note manquante"),
ErreurNote::PasUnNombre(t) => write!(f, "« {t} » n'est pas une note"),
ErreurNote::HorsBornes(n) => write!(f, "{n} n'est pas entre 0 et 20"),
}
}
}
fn lire_note(texte: &str) -> Result<f64, ErreurNote> {
let t = texte.trim().replace(',', ".");
if t.is_empty() {
return Err(ErreurNote::Vide);
}
let n: f64 = t.parse().map_err(|_| ErreurNote::PasUnNombre(texte.to_string()))?;
if !(0.0..=20.0).contains(&n) {
return Err(ErreurNote::HorsBornes(n));
}
Ok(n)
}
struct Ligne {
nom: String,
note: f64,
}
fn analyser_ligne(ligne: &str) -> Result<Ligne, String> {
let champs: Vec<&str> = ligne.split(';').collect();
let [nom, _matiere, note] = champs[..] else {
return Err(format!("3 champs attendus, {} trouvé(s)", champs.len()));
};
if nom.trim().is_empty() {
return Err(String::from("nom manquant"));
}
let note = lire_note(note).map_err(|e| e.to_string())?;
Ok(Ligne { nom: nom.trim().to_string(), note })
}
fn main() {
let mut par_eleve: HashMap<String, Vec<f64>> = HashMap::new();
let mut rejets: Vec<String> = Vec::new();
for (i, ligne) in EXPORT.lines().enumerate().skip(1) {
match analyser_ligne(ligne) {
Ok(l) => par_eleve.entry(l.nom).or_default().push(l.note),
Err(motif) => rejets.push(format!("ligne {} : {motif}", i + 1)),
}
}
let mut noms: Vec<&String> = par_eleve.keys().collect();
noms.sort();
println!("{:<8} {:>6} {:>8}", "Élève", "Notes", "Moyenne");
for nom in noms {
let notes = &par_eleve[nom];
let moyenne = notes.iter().sum::<f64>() / notes.len() as f64;
println!("{:<8} {:>6} {:>8.2}", nom, notes.len(), moyenne);
}
println!("\n{} ligne(s) rejetée(s) :", rejets.len());
for r in &rejets {
println!(" {r}");
}
}
Explication
Le traitement est découpé en couches : lire_note valide une valeur, analyser_ligne valide une ligne entière, main agrège. Chaque couche renvoie un Result et la boucle principale ne fait que trier les succès et les échecs. let [nom, _matiere, note] = champs[..] else vérifie en une ligne qu’il y a exactement trois champs. or_default() crée un vecteur vide pour un élève encore inconnu.
Pour aller plus loin
Ce motif « valider chaque ligne, garder les rejets, produire un rapport » est exactement celui des imports de comptes utilisateurs, de fichiers de commandes ou de relevés bancaires.
🧠 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.