iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
Rust · Intermédiaire

Rust

Apprendre Rust de zéro jusqu'à un vrai outil en ligne de commande : ownership, emprunts, enums, gestion d'erreurs, traits, génériques, durées de vie, itérateurs, tests, threads et async avec Tokio.

RustCargoédition 2024serdeclapTokio
01Introduction et installation02Variables, types, fonctions et contrôle de flux03Ownership et emprunts04Structs, enums et pattern matching05Collections et gestion d'erreurs06Traits et génériques07Durées de vie et pointeurs intelligents08Closures et itérateurs09Modules, crates et tests10Concurrence : threads, canaux et async11Projet : un gestionnaire de tâches en ligne de commande
Chapitre 5·35 min

Collections et gestion d'erreurs

Deux sujets du quotidien dans ce chapitre. Les collections de la bibliothèque standard, Vec, String et HashMap, qui couvrent la grande majorité des besoins. Et la gestion d'erreurs, où Rust fait un choix radical : pas d'exceptions. Une fonction qui peut échouer le dit dans son type de retour, et l'appelant est obligé d'en tenir compte.

Vec : la liste dynamique

fn main() {
    let mut notes: Vec<u32> = Vec::new();
    notes.push(14);
    notes.push(9);
    notes.push(17);
 
    let autres = vec![12, 15];        // macro de création
    notes.extend(&autres);
 
    println!("{} notes : {:?}", notes.len(), notes);
    println!("première : {}", notes[0]);
    println!("dixième : {:?}", notes.get(9)); // None, pas de plantage
 
    notes.sort();
    notes.retain(|&n| n >= 10);       // garder les notes >= 10
    println!("{notes:?}");
}

Deux façons d'accéder à un élément : notes[i] panique si l'indice n'existe pas, notes.get(i) renvoie un Option. Utilisez l'indice quand une absence serait un bug, get quand elle est un cas normal.

Pour parcourir un vecteur, trois formes, qui suivent les règles d'ownership du chapitre 3 :

fn main() {
    let mut v = vec![1, 2, 3];
    for x in &v { println!("{x}"); }      // emprunt en lecture
    for x in &mut v { *x *= 10; }         // emprunt en écriture
    for x in v { println!("{x}"); }       // consomme v, inutilisable ensuite
}

Le * dans *x *= 10 déréférence : x est une référence &mut i32, et on modifie l'entier vers lequel elle pointe.

String en pratique

fn main() {
    let mut s = String::from("Bonjour");
    s.push_str(", monde");
    s.push('!');
 
    let prenom = "Ada";
    let message = format!("{s} Je suis {prenom}."); // comme println!, renvoie une String
 
    println!("{message}");
    println!("{}", message.to_uppercase());
    println!("{}", message.contains("monde"));
    println!("{}", message.replace("monde", "Rust"));
 
    for mot in message.split_whitespace() {
        print!("[{mot}] ");
    }
    println!();
}

format! est la façon la plus lisible de construire une chaîne. Et rappel du chapitre 3 : une fonction qui lit du texte prend &str.

HashMap : le dictionnaire

use std::collections::HashMap;
 
fn main() {
    let texte = "le chat et le chien et le poisson";
    let mut compteur: HashMap<&str, u32> = HashMap::new();
 
    for mot in texte.split_whitespace() {
        *compteur.entry(mot).or_insert(0) += 1;
    }
 
    println!("{:?}", compteur.get("le"));   // Some(3)
 
    let mut tries: Vec<_> = compteur.iter().collect();
    tries.sort_by(|a, b| b.1.cmp(a.1).then(a.0.cmp(b.0)));
    for (mot, n) in tries {
        println!("{mot} : {n}");
    }
}

L'API entry est l'idiome à retenir : elle cherche la clé, l'insère avec une valeur par défaut si elle manque, et renvoie une référence modifiable vers la valeur. Une seule recherche dans la table, au lieu de deux avec un if contains_key.

Une HashMap ne garde aucun ordre. Si vous en avez besoin, BTreeMap trie ses clés, au prix d'un accès un peu plus lent.

Deux sortes d'erreurs

Rust sépare nettement :

SorteExempleMécanisme
Bug, état impossibleIndice hors limites, invariant violépanic! : le programme s'arrête
Échec attenduFichier absent, saisie invalide, réseau coupéResult<T, E> : une valeur à traiter

Une panique n'est pas faite pour être rattrapée. Elle signale un bug. Tout ce qui peut raisonnablement échouer renvoie un Result.

Result

Result est un enum, cousin d'Option :

enum Result<T, E> {
    Ok(T),
    Err(E),
}
fn main() {
    let saisies = ["42", "abc", "-7"];
    for s in saisies {
        match s.parse::<i32>() {
            Ok(n) => println!("{s} -> {n}"),
            Err(e) => println!("{s} -> erreur : {e}"),
        }
    }
}
42 -> 42
abc -> erreur : invalid digit found in string
-7 -> -7

Comme avec Option, impossible d'utiliser le nombre sans avoir décidé quoi faire de l'erreur. Le type de retour de parse annonce l'échec possible ; un lecteur le voit sans lire la documentation.

L'opérateur ?

Écrire un match pour chaque opération qui peut échouer deviendrait vite illisible. L'opérateur ? fait la chose la plus courante : si c'est Ok, il extrait la valeur ; si c'est Err, il renvoie l'erreur à l'appelant immédiatement.

use std::fs;
use std::io;
 
fn lire_premiere_ligne(chemin: &str) -> Result<String, io::Error> {
    let contenu = fs::read_to_string(chemin)?;
    let ligne = contenu.lines().next().unwrap_or("").to_string();
    Ok(ligne)
}
 
fn main() {
    match lire_premiere_ligne("Cargo.toml") {
        Ok(l) => println!("première ligne : {l}"),
        Err(e) => println!("impossible de lire : {e}"),
    }
}

? ne fonctionne que dans une fonction qui renvoie elle-même un Result (ou un Option). main peut en renvoyer un :

use std::fs;
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let contenu = fs::read_to_string("Cargo.toml")?;
    let lignes = contenu.lines().count();
    println!("{lignes} lignes");
    Ok(())
}

Box<dyn std::error::Error> signifie « n'importe quel type d'erreur ». Pratique pour un petit programme, on verra mieux plus bas.

Définir ses propres erreurs

Une bibliothèque doit dire précisément ce qui peut échouer. On décrit les cas avec un enum :

use std::fmt;
 
#[derive(Debug)]
enum ErreurCommande {
    QuantiteNulle,
    StockInsuffisant { demande: u32, disponible: u32 },
}
 
impl fmt::Display for ErreurCommande {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ErreurCommande::QuantiteNulle => write!(f, "la quantité doit être positive"),
            ErreurCommande::StockInsuffisant { demande, disponible } => {
                write!(f, "stock insuffisant : {demande} demandés, {disponible} disponibles")
            }
        }
    }
}
 
impl std::error::Error for ErreurCommande {}
 
fn commander(quantite: u32, stock: u32) -> Result<u32, ErreurCommande> {
    if quantite == 0 {
        return Err(ErreurCommande::QuantiteNulle);
    }
    if quantite > stock {
        return Err(ErreurCommande::StockInsuffisant { demande: quantite, disponible: stock });
    }
    Ok(stock - quantite)
}
 
fn main() {
    for q in [3, 0, 50] {
        match commander(q, 10) {
            Ok(reste) => println!("commande OK, reste {reste}"),
            Err(e) => println!("refusée : {e}"),
        }
    }
}

L'appelant peut faire un match sur les variantes et réagir différemment à chaque cas. C'est ce qu'une exception attrapée par type essaie de faire, avec la garantie en plus que tous les cas sont connus.

thiserror et anyhow

Écrire Display à la main devient vite fastidieux. Deux crates sont devenues des standards de fait :

CratePourIdée
thiserrorLes bibliothèquesGénère Display et Error à partir d'attributs
anyhowLes applicationsUn type d'erreur unique, avec du contexte ajouté au fil des appels
cargo add thiserror anyhow
use thiserror::Error;
 
#[derive(Debug, Error)]
enum ErreurCommande {
    #[error("la quantité doit être positive")]
    QuantiteNulle,
    #[error("stock insuffisant : {demande} demandés, {disponible} disponibles")]
    StockInsuffisant { demande: u32, disponible: u32 },
}

Et côté application :

use anyhow::{Context, Result};
use std::fs;
 
fn charger_config(chemin: &str) -> Result<String> {
    let contenu = fs::read_to_string(chemin)
        .with_context(|| format!("lecture de la configuration {chemin}"))?;
    Ok(contenu)
}
 
fn main() -> Result<()> {
    let config = charger_config("config.toml")?;
    println!("{config}");
    Ok(())
}

Si le fichier manque, le message affiché enchaîne les contextes :

Error: lecture de la configuration config.toml
 
Caused by:
    No such file or directory (os error 2)

Ma règle : thiserror dans le code qu'un autre programme appellera, anyhow dans main et autour.

unwrap et expect

unwrap() extrait la valeur ou panique. expect("message") fait pareil avec un message lisible. Ils ont leur place dans les tests, les prototypes, et quand une erreur est réellement impossible :

let port: u16 = "8080".parse().expect("littéral valide");

Partout ailleurs, un unwrap est une panique qui attend son heure. Cherchez-les avant chaque mise en production.

Exercice

Écrivez moyenne(notes: &str) -> Result<f64, String> qui reçoit des notes séparées par des virgules ("12, 15, 9"), renvoie une erreur si une note n'est pas un nombre ou si la liste est vide, et la moyenne sinon.

fn moyenne(notes: &str) -> Result<f64, String> {
    let mut valeurs = Vec::new();
    for morceau in notes.split(',') {
        let morceau = morceau.trim();
        let n: f64 = morceau
            .parse()
            .map_err(|_| format!("« {morceau} » n'est pas une note"))?;
        valeurs.push(n);
    }
    if valeurs.is_empty() {
        return Err("aucune note".to_string());
    }
    Ok(valeurs.iter().sum::<f64>() / valeurs.len() as f64)
}
 
fn main() {
    println!("{:?}", moyenne("12, 15, 9"));   // Ok(12.0)
    println!("{:?}", moyenne("12, quinze"));  // Err("« quinze » n'est pas une note")
}

map_err transforme l'erreur de parse en notre message, puis ? la renvoie. Remarquez que "" donne l'erreur « n'est pas une note » plutôt que « aucune note » : split renvoie un morceau vide. Le traiter proprement est un bon prolongement.


Vec, String et HashMap couvrent l'essentiel des besoins. Côté erreurs, le contrat est clair : panique pour les bugs, Result pour tout le reste, ? pour propager sans bruit. Le chapitre suivant présente les traits et les génériques, qui permettent d'écrire du code valable pour plusieurs types.

Précédent
Structs, enums et pattern matching
Suivant
Traits et génériques

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main