MémentoAuteur : D. Dambreville · didierdambreville.github.io · licence CC BY 4.0

Rust

Le langage · possession et emprunt · types et traits · erreurs · collections · Cargo · concurrence
let x = 5; chasse fixe, gras : code à taper tel quel
nom italique : valeur à remplacer
// → 42 commentaire : résultat obtenu
E0382 code d’erreur du compilateur
! piège fréquent ou action risquée

Installer et démarrer

rustupinstallateur officiel et gestionnaire de versions (rustup.rs)
rustup updatemettre à jour compilateur et outils
cargo new projetcréer un projet (--lib : une bibliothèque)
cargo runcompiler et lancer
cargo checkvérifier sans produire d’exécutable (rapide)
cargo build --releasecompilation optimisée → target/release/
cargo testlancer les tests
cargo fmtmettre le code en forme
cargo clippyconseils de qualité (analyseur)
rustup doc --stddocumentation de la bibliothèque standard, hors ligne

Windows : rustup-init.exe ; il propose d’installer les outils Visual Studio (éditeur de liens MSVC) requis.

Une version stable toutes les 6 semaines ; au 29 septembre 2026 : 1.98. Édition courante : 2024 (depuis Rust 1.85).

Premier programme

// src/main.rs
fn main() {
    let nom = "monde";
    println!("Bonjour, {nom} !");
}
  • fn main() : point d’entrée du programme.
  • println! : le ! signale une macro.
  • Instruction terminée par ; ; blocs entre { }.
  • Commentaires : // ligne · /* */ bloc · /// documentation de l’élément qui suit.
  • Noms : snake_case variables, fonctions · CamelCase types, traits · MAJUSCULES constantes.

Variables

let x = 5;liaison immuable (par défaut)
let mut n = 0;liaison modifiable
let x: i64 = 5;type explicite (sinon déduit)
let x = x * 2;masquage : nouvelle variable de même nom, type libre
const MAX: u32 = 100;constante : type obligatoire, calculée à la compilation
static NOM: &str = "a";valeur globale unique
let (a, b) = (1, 2);déstructuration
let _x = 1;_ en tête : pas d’avertissement si inutilisée

! Réaffecter une liaison sans mut : erreur E0384.

Types scalaires

i8 i16 i32 i64 i128entiers signés ; i32 par défaut
u8 u16 u32 u64 u128entiers non signés ; u8 = octet
isize usizetaille d’une adresse (64 bits sur un PC actuel) ; index et longueurs : usize
f32 f64flottants IEEE 754 ; f64 par défaut
booltrue · false
charun caractère Unicode, 4 octets : 'é'
()« unité » : absence de valeur

Littéraux

1_000_000_ : séparateur visuel
0xff 0o77 0b1010hexadécimal · octal · binaire
255u8 2.5f32suffixe de type
b'A' b"abc"octet u8 · chaîne d’octets
'\n' '\t' '\\' '\u{e9}'caractères échappés (\u{e9} = é)
r"C:\d" · r#"a "b""#chaînes brutes : \ et " pris à la lettre

Bornes : i32::MAX, u8::MIN… ! Dépassement d’entier : panique en mode debug, résultat « rebouclé » en --release. Choisir : checked_add (→ Option), wrapping_add, saturating_add.

Types composés

(i32, f64, char)tuple : types mêlés, taille fixe ; t.0, t.1
[i32; 5]tableau : taille fixe ; [0; 5] = cinq zéros
&[i32]tranche (slice) : vue sur une suite contiguë ; &a[1..3]
&strtranche de texte UTF-8 ; littéraux "…"
Stringtexte possédé, modifiable (tas)
Vec<T>tableau dynamique possédé (tas)

a[i] hors limites : panique, jamais de lecture hasardeuse. a.get(i) renvoie une Option.

Conversions

x as f64entre types primitifs, sans contrôle
f64::from(x) · x.into()sans perte possible (From / Into)
u8::try_from(x)faillible → Result
"42".parse::<i32>()texte → nombre : Result
n.to_string()nombre → String
s.as_str() · &sString → &str
s.to_string()&str → String (ou String::from(s))

! as tronque : 300_i32 as u8 → 44 ; -1_i32 as u32 → 4294967295 ; 2.9_f64 as i32 → 2 ; 300.0_f64 as u8 → 255.

Afficher : println! et format!

{}affichage « utilisateur » (Display)
{:?} · {:#?}débogage (Debug) · indenté
{x} · {0} · {nom}variable · position · argument nommé
{:>8} {:<8} {:^8}à droite · à gauche · centré sur 8
{:08.2}8 caractères, zéros devant, 2 décimales
{:+}signe toujours affiché
{:x} {:X} {:o} {:b}hexadécimal · majuscules · octal · binaire
{:#x} · {:#b}avec préfixe 0x · 0b
{:e}notation scientifique
{:>w$} · {:.p$}largeur · précision prises dans w · p
{{ · }}accolade littérale

Macros d’usage courant

print! · println!sortie standard
eprint! · eprintln!sortie d’erreur
format!(…)même syntaxe → String
dbg!(expr)affiche fichier, ligne, expression et valeur (sur stderr), puis la renvoie
write! · writeln!vers un fichier ou un tampon
vec![1, 2, 3]construire un Vec
assert!(c) · assert_eq!(a, b)vérifier, sinon paniquer
todo!()code à écrire : panique si atteint
unreachable!()branche jugée impossible

Lire le compilateur

  1. Lire la première erreur : les suivantes en découlent souvent.
  2. Code E0382 → rustc --explain E0382 : explication et exemples.
  3. Suivre les lignes help: : le correctif y est souvent proposé.
  4. cargo check pour itérer vite ; cargo clippy pour les tournures idiomatiques.
E0308types incompatiblesconversion ? ; final en trop ?
E0277trait non implémentéderive, impl, autre type
E0599méthode introuvabletype exact ? trait importé ?
E0425nom inconnuorthographe, portée
E0432 · E0433import · chemin non résoluuse, mod, dépendance

Erreurs d’emprunt : page 3. Hors blocs unsafe, le compilateur écarte des classes entières d’erreurs mémoire : pointeur pendant, double libération, accès concurrent non synchronisé.

2 · Opérateurs, fonctions, contrôle du flux

Mémento — Rust

Opérateurs

+ - * / %arithmétique ; entre entiers, / division entière : 7 / 2 → 3, % reste
== != < > <= >=comparaison
&& || !logique (évaluation paresseuse)
& | ^ << >>bit à bit ; !n : complément
+= -= *= /= %=affectation composée
a..b · a..=bintervalle exclusif · inclusif
&x · &mut x · *remprunter · en modification · déréférencer
?propager une erreur (page 5)
a::bchemin : std::fs::read, String::new

Ni ++ ni -- : n += 1. Puissance : 2_i32.pow(10), x.powf(0.5), x.sqrt(). Arrondis : round, floor, ceil · bornes : min, max, clamp · reste positif : rem_euclid (-7 % 3 → -1). ! Flottants : comparer (a - b).abs() < 1e-9, pas a == b après un calcul.

Tout est expression

let signe = if n < 0 { "négatif" } else { "positif" };

let carre = {
    let a = 3;
    a * a            // pas de ; : valeur du bloc
};                   // carre = 9

Un bloc vaut sa dernière expression sans ; ; avec ;, il vaut (). Les branches d’un if doivent avoir le même type.

Fonctions

fn aire(l: f64, h: f64) -> f64 {
    l * h                           // valeur renvoyée
}

fn diviser(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        return None;                // sortie anticipée
    }
    Some(a / b)
}

fn saluer(nom: &str) {              // renvoie ()
    println!("Bonjour {nom}");
}
  • Types des paramètres obligatoires ; type de retour après ->.
  • Ni paramètres par défaut ni surcharge : Option, noms distincts, constructeurs.
  • ! fn f() -> i32 { 5; } : le ; fait renvoyer () → E0308.
  • Retour -> ! : la fonction ne revient jamais (panic!, std::process::exit).

Fermetures (closures)

let double = |x| x * 2;
let somme = |a: i32, b: i32| -> i32 { a + b };
let seuil = 10;
let grand = |x: &i32| *x > seuil;   // capture seuil
let f = move || println!("{seuil}");// prend possession
Fnlit ce qu’elle capture ; appelable à volonté
FnMutmodifie ce qu’elle capture
FnOnceconsomme ce qu’elle capture : un seul appel
fn(i32) -> i32pointeur de fonction (aucune capture)

move : indispensable quand la fermeture survit à la portée courante (thread, valeur renvoyée). Accepter une fermeture : fn appliquer(f: impl Fn(i32) -> i32).

Boucles

for i in 0..5 { }                   // 0, 1, 2, 3, 4
for i in 1..=5 { }                  // 1 à 5 inclus
for i in (0..10).rev() { }          // 9 à 0
for i in (0..10).step_by(3) { }     // 0, 3, 6, 9
for (i, x) in v.iter().enumerate() { }

while n > 0 { n -= 1; }

let mut k = 0;
let r = loop {                      // boucle sans fin…
    k += 1;
    if k == 10 { break k * 2; }     // …qui renvoie 20
};

'externe: for x in 0..5 {           // étiquette
    for y in 0..5 {
        if x + y > 6 { break 'externe; }
        if y > x { continue 'externe; }
    }
}

for x in &v parcourt sans consommer ; for x in v consomme v (inutilisable ensuite) ; for x in &mut v permet de modifier : *x += 1.

match : filtrage par motif

match n {
    0 => println!("zéro"),
    1 | 2 => println!("un ou deux"),
    3..=9 => println!("chiffre"),
    x if x < 0 => println!("négatif : {x}"),
    grand @ 100.. => println!("grand : {grand}"),
    _ => println!("autre"),
}

let pays = match code {             // une expression
    'F' => "France",
    'D' => "Allemagne",
    _ => "inconnu",
};
_n’importe quoi, sans lier
xlie la valeur à x
a | bl’un ou l’autre
1..=5 · 'a'..='z'intervalle inclusif
x if conditiongarde : condition supplémentaire
n @ 1..=9tester et lier à la fois
(a, _, c)tuple
[premier, .., dernier]tableau ou tranche ; .. : le reste
Point { x, y: 0, .. }structure : champs lus, champs testés
Some(v) · Err(e)variante d’une énumération
&xdéstructurer une référence

! Un match doit être exhaustif : tout cas possible couvert, sinon E0004. _ en dernier recueille le reste.

Raccourcis de filtrage

if let Some(x) = option {           // un seul motif
    println!("{x}");
} else {
    println!("rien");
}

let Some(x) = option else {         // let … else
    return;                         // doit sortir
};                                  // x reste utilisable

while let Some(sommet) = pile.pop() {
    println!("{sommet}");
}

if let Some(x) = option && x > 10 { }   // édition 2024

let voyelle = matches!(c, 'a' | 'e' | 'i' | 'o' | 'u');

Le bloc else d’un let … else doit diverger : return, break, continue, panic!. Chaînes if let … && : édition 2024, Rust 1.88 ou plus.

3 · Possession, emprunt, durées de vie

Mémento — Rust

Les trois règles de la possession (ownership)

  1. Chaque valeur a un seul propriétaire : une variable, un champ, un élément de collection.
  2. Affecter la valeur ou la passer à une fonction la déplace (move) : l’ancien nom devient inutilisable.
  3. Quand le propriétaire sort de sa portée, la valeur est libérée (drop), automatiquement : ni ramasse-miettes ni free.

Les types Copy (entiers, flottants, bool, char, &T, tuples et tableaux de types Copy) sont copiés au lieu d’être déplacés. Les autres (String, Vec, Box…) se dupliquent explicitement avec .clone() : une copie coûteuse reste visible.

let s1 = String::from("salut");
let s2 = s1;           // déplacement : s1 n'est plus utilisable

   s1 (invalidée)        s2                     tas
 +-----+-------+     +-----+-------+     +---+---+---+---+---+
 | ptr |   x   |     | ptr |   o---+---> | s | a | l | u | t |
 | len |   5   |     | len |   5   |     +---+---+---+---+---+
 | cap |   5   |     | cap |   5   |
 +-----+-------+     +-----+-------+
         pile (stack)

Déplacer, copier, cloner

let s1 = String::from("salut");
let s2 = s1;                        // déplacement
// println!("{s1}");                // E0382 : valeur déplacée

let s3 = s2.clone();                // copie profonde
println!("{s2} {s3}");              // les deux sont valides

let a = 5;
let b = a;                          // i32 est Copy
println!("{a} {b}");                // a reste valide

fn prendre(s: String) { }           // prend possession
prendre(s3);                        // s3 inutilisable ensuite

Emprunter : les références

fn longueur(s: &String) -> usize { s.len() }  // lit
fn crier(s: &mut String) { s.push('!'); }     // modifie

let mut s = String::from("salut");
let n = longueur(&s);               // emprunt partagé
crier(&mut s);                      // emprunt exclusif
Règle d’or — à un instant donné : soit autant de références partagées &T qu’on veut, soit une seule référence exclusive &mut T. Et une référence ne vit jamais plus longtemps que la valeur qu’elle désigne.
let mut v = vec![1, 2, 3];
let premier = &v[0];                // emprunt partagé…
v.push(4);                          // E0502 : exclusif
println!("{premier}");              // …encore utilisé ici

Pourquoi ? push peut réallouer le vecteur : premier désignerait une mémoire libérée. Correctifs : utiliser premier avant le push, ou copier la valeur (let premier = v[0];).

Un emprunt dure jusqu’à sa dernière utilisation, pas jusqu’à la fin du bloc.

Durées de vie ('a)

fn plus_long<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

struct Extrait<'a> {
    texte: &'a str,                 // la structure emprunte
}
  • 'a ne change aucune durée : il décrit le lien entre les références (ici : le résultat vit autant que la plus courte des deux).
  • Élision : une seule référence en paramètre, ou &self → annotation inutile.
  • 'static : valable pendant tout le programme (littéraux "…").

Portée et libération

{
    let a = String::from("a");
    let b = vec![1, 2, 3];
    // … a et b utilisables ici
}                       // fin de portée : b, puis a, libérés
let v = vec![0; 1000];
drop(v);                // libération anticipée

Ordre inverse de la déclaration. impl Drop : agir à la libération (fermer un fichier, relâcher un verrou).

Choisir le type d’un paramètre

&strlire un texte (accepte &String et les littéraux)
&[T]lire une suite (accepte &Vec<T> et les tableaux)
&mut Tmodifier sur place
Tgarder la valeur (la stocker) ; petites valeurs Copy

Renvoyer une valeur possédée (String, Vec<T>) plutôt qu’une référence vers une variable locale.

Pointeurs intelligents et partage

Box<T>valeur sur le tas, un seul propriétairetype récursif, dyn Trait
Rc<T>plusieurs propriétaires (compteur de références)partage dans un thread
Arc<T>idem, compteur atomiquepartage entre threads
Cell<T> RefCell<T>modifier à travers & (mutabilité intérieure)contrôle à l’exécution
Mutex<T> RwLock<T>mutabilité intérieure verrouilléeentre threads
Weak<T>référence non possédante vers un Rc / Arccasser les cycles
Cow<'a, T>emprunté ou possédé selon le besoinéviter une copie

Associations classiques : Rc<RefCell<T>> (un thread) · Arc<Mutex<T>> (plusieurs threads).

! RefCell : deux borrow_mut() simultanés → panique. Deux Rc qui se désignent mutuellement ne sont jamais libérés : fuite mémoire (utiliser Weak).

Prendre une valeur hors d’un emprunt (E0507)

&v[i]emprunter au lieu de prendre
v[i].clone()copier l’élément (coût d’une copie)
mem::take(&mut v[i])prendre, en laissant la valeur par défaut ("", 0, vide)
option.take()prendre le contenu, en laissant None
v.into_iter() · v.drain(..)parcourir en prenant possession · en vidant

Codes d’erreur fréquents

E0382valeur utilisée après déplacementemprunter &, cloner, réordonner
E0499deux emprunts &mut simultanésraccourcir le premier
E0502&mut pendant un emprunt &finir de lire avant d’écrire
E0505déplacement d’une valeur empruntéeattendre la fin de l’emprunt
E0507sortie d’une valeur depuis une référence&, .clone(), mem::take
E0596&mut sur une variable non mutlet mut
E0597valeur libérée trop tôtallonger sa portée
E0515référence vers une variable locale renvoyéerenvoyer la valeur possédée
E0106durée de vie manquanteannoter 'a ou posséder

Détail et exemples : rustc --explain code ou doc.rust-lang.org/error_codes.

4 · Structures, énumérations, traits, génériques

Mémento — Rust

Structures et méthodes

#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
    fn new(x: f64, y: f64) -> Self { // fonction associée
        Self { x, y }                // raccourci de x: x
    }
    fn norme(&self) -> f64 { self.x.hypot(self.y) }
    fn deplacer(&mut self, dx: f64) { self.x += dx; }
    fn eclater(self) -> (f64, f64) { (self.x, self.y) }
}

let mut p = Point::new(3.0, 4.0);
println!("{}", p.norme());           // 5
p.deplacer(1.0);
let q = Point { y: 0.0, ..p.clone() }; // autres champs de p
let Point { x, y } = q;              // déstructurer
&selfla méthode lit l’instance
&mut selfla méthode la modifie
selfla méthode la consomme
Selfle type lui-même, dans un impl
struct Metres(f64);structure-tuple : m.0 ; « newtype »
struct Vide;structure unité, sans champ
type Km = f64;alias : autre nom du même type

Énumérations

use std::f64::consts::PI;

enum Forme {
    Cercle { rayon: f64 },
    Rectangle(f64, f64),
    Point,
}

impl Forme {
    fn aire(&self) -> f64 {
        match self {
            Forme::Cercle { rayon } => PI * rayon * rayon,
            Forme::Rectangle(l, h) => l * h,
            Forme::Point => 0.0,
        }
    }
}

Une valeur d’énumération est une de ses variantes, chacune avec ses propres données : idéal pour modéliser des états.

Dans le prélude, sans use : enum Option<T> { None, Some(T) } et enum Result<T, E> { Ok(T), Err(E) }. Rust n’a pas de null.

Traits : comportements partagés

trait Decrire {
    fn nom(&self) -> String;         // à fournir
    fn decrire(&self) -> String {    // fournie par défaut
        format!("Je suis {}", self.nom())
    }
}

impl Decrire for Point {
    fn nom(&self) -> String { "un point".to_string() }
}

use std::fmt;
impl fmt::Display for Point {        // active {} et to_string
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

Implémenter un trait pour un type : permis si le trait ou le type est défini dans votre crate (règle de l’orphelin).

Génériques et contraintes

fn plus_grand<T: PartialOrd>(v: &[T]) -> &T {
    let mut max = &v[0];
    for x in v {
        if x > max { max = x; }
    }
    max
}

fn afficher(x: impl fmt::Display) { println!("{x}"); }

fn resumer<T>(x: &T) -> String
where
    T: Decrire + Clone,
{
    x.decrire()
}

struct Pile<T> { elements: Vec<T> }
impl<T> Pile<T> {
    fn empiler(&mut self, x: T) { self.elements.push(x); }
}
T: Traitcontrainte : T doit implémenter Trait
T: A + Bplusieurs contraintes
where …contraintes nombreuses, écrites à part
x: impl Traitparamètre : raccourci d’un générique
-> impl Traitrenvoie « un type qui implémente Trait » (itérateur…)
&dyn Trait · Box<dyn Trait>objet-trait : type choisi à l’exécution

Générique : une copie du code par type, à la compilation (monomorphisation) — rapide. dyn : un seul code, appel indirect ; permet les collections hétérogènes : Vec<Box<dyn Decrire>>.

Traits standard à connaître

Debug{:?}derive
Display{}, .to_string()à écrire
Clone · Copy.clone() · copie implicitederive
PartialEq · Eq==, !=derive
PartialOrd · Ord<, >, .sort(), .max()derive
Hashclé de HashMap (avec Eq)derive
DefaultT::default()derive
From · Intoconversions ; From fournit Intoà écrire
FromStr"…".parse()à écrire
Iteratorboucle for, adaptateurs : fournir next()à écrire
Dropcode exécuté à la libérationà écrire
Deref*x ; méthodes transmises (Box, String)à écrire
Add, Mul, Neg…surcharge des opérateurs (std::ops)à écrire
Errortype d’erreur (avec Debug + Display)à écrire
Send · Synctransfert · partage entre threadsauto­matique

! f64 n’implémente ni Eq ni Ord (à cause de NaN) : v.sort_by(|a, b| a.total_cmp(b)).

Visibilité

(rien)privé : visible dans le module et ses sous-modules
pubpublic
pub(crate)visible dans toute la crate
pub(super)visible dans le module parent

! Un pub struct garde ses champs privés : mettre pub devant chaque champ voulu.

5 · Gestion des erreurs, entrées-sorties

Mémento — Rust

Trois situations

panic!irrécupérable : bogue, invariant violé ; le thread s’arrête
Result<T, E>récupérable : l’appelant décide (fichier absent, saisie invalide, réseau)
Option<T>absence de valeur, sans erreur (recherche vide, champ facultatif)

Ni exceptions ni null : les échecs possibles se lisent dans la signature des fonctions.

Quand paniquer ?

  • Oui : bogue, invariant impossible, tests, prototypes ; expect sur une hypothèse démontrée.
  • Non : tout ce qui vient de l’extérieur (fichier, saisie, réseau, données).
  • Paniques implicites : index hors limites, division entière par zéro, dépassement (debug), unwrap sur None / Err, tranche de str coupant un caractère.
  • Voir la pile d’appels : variable d’environnement RUST_BACKTRACE=1.

L’opérateur ?

use std::error::Error;
use std::fs;

fn lire_nombre(chemin: &str) -> Result<i64, Box<dyn Error>> {
    let texte = fs::read_to_string(chemin)?;  // io::Error
    let n: i64 = texte.trim().parse()?;       // ParseIntError
    Ok(n * 2)
}

fn main() -> Result<(), Box<dyn Error>> {
    let n = lire_nombre("nombre.txt")?;
    println!("{n}");
    Ok(())
}
  • x? : si Ok(v), donne v ; si Err(e), renvoie aussitôt Err(e.into()).
  • Fonctionne aussi sur Option dans une fonction qui renvoie Option.
  • Seulement dans une fonction qui renvoie Result ou Option (sinon E0277).
  • main peut renvoyer Result : une erreur y est affichée (format Debug) et le programme se termine avec un code d’échec.

Extraire ou transformer : Option et Result

match · if let · let elsetraiter chaque cas explicitement
?propager à l’appelant
.unwrap()! la valeur, ou panique (prototypes, tests)
.expect("raison")idem, avec l’hypothèse expliquée
.unwrap_or(v)la valeur, sinon v
.unwrap_or_else(|| …)sinon calculée (seulement si besoin)
.unwrap_or_default()sinon 0, "", vide…
.is_some() · .is_ok()tests (et is_none, is_err)
.is_some_and(|x| …)présente et conforme
.map(|x| …)transformer la valeur si présente
.and_then(|x| …)enchaîner une opération faillible
.or(b) · .or_else(|| …)solution de repli
.filter(|x| …)Option : garder si la condition est vraie
.map_err(|e| …)Result : transformer l’erreur
.ok_or(e) · .ok_or_else(|| e)Option → Result
.ok() · .err()Result → Option
.as_deref()&Option<String> → Option<&str>
.take()extraire, en laissant None
.copied() · .cloned()Option<&T> → Option<T>

Une suite de Result → un seul : .collect::<Result<Vec<_>, _>>() (arrêt à la première erreur).

Définir son type d’erreur

use std::{fmt, io};

#[derive(Debug)]
enum ErreurConfig {
    Absente(String),
    Syntaxe(usize),
    Io(io::Error),
}

impl fmt::Display for ErreurConfig {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Absente(c) => write!(f, "clé {c} absente"),
            Self::Syntaxe(n) => write!(f, "erreur ligne {n}"),
            Self::Io(e) => write!(f, "entrée-sortie : {e}"),
        }
    }
}

impl std::error::Error for ErreurConfig {}

impl From<io::Error> for ErreurConfig {  // pour ?
    fn from(e: io::Error) -> Self { Self::Io(e) }
}
  • Bibliothèque : type d’erreur précis (enum), pour que l’appelant distingue les cas.
  • Application : Box<dyn Error> suffit souvent.
  • Crates tierces répandues : thiserror (écrit ce code) et anyhow (erreurs d’application, contexte) — à évaluer comme tout code externe.

Fichiers et système

fs::read_to_string(p)?tout le fichier → String
fs::write(p, contenu)?créer ou écraser
fs::read(p)?octets bruts → Vec<u8>
File::open(p)? · File::create(p)?ouvrir en lecture · créer (écrase)
OpenOptions::new().append(true).create(true).open(p)?ajouter à la fin
fs::create_dir_all(p)?créer les dossiers manquants
fs::read_dir(p)?lister un dossier
Path::new(p).exists()chemins portables : Path, PathBuf
std::env::args()arguments ; le premier : le programme
std::env::var("NOM")variable d’environnement → Result
std::process::exit(1)quitter avec un code de retour
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};

for ligne in BufReader::new(File::open("a.txt")?).lines() {
    let ligne = ligne?;              // chaque ligne : Result
    println!("{ligne}");
}
let mut sortie = File::create("rapport.txt")?;
writeln!(sortie, "total : {}", 42)?;

Lire le clavier

print!("Votre âge : ");
io::stdout().flush()?;               // afficher aussitôt
let mut saisie = String::new();
io::stdin().read_line(&mut saisie)?;
let age: u32 = saisie.trim().parse()?; // trim : retire \n

write!, writeln! et flush exigent le trait Write : use std::io::Write.

6 · Collections, texte et itérateurs

Mémento — Rust

Vec<T> : tableau dynamique

Vec::new() · vec![1, 2] · vec![0; n]créer
Vec::with_capacity(n)créer en réservant la place
v.push(x) · v.pop()ajouter à la fin · retirer (→ Option)
v[i] · v.get(i)accès : panique hors limites · → Option<&T>
v.first() · v.last()premier · dernier → Option
v.len() · v.is_empty()taille · vide ?
v.insert(i, x) · v.remove(i)au rang i (décale la suite)
v.swap_remove(i)retirer sans décaler (le dernier prend la place)
v.contains(&x)présence
v.sort() · v.sort_unstable()trier (éléments Ord)
v.sort_by_key(|p| p.age)trier selon une clé
v.sort_by(|a, b| b.cmp(a))ordre libre (ici décroissant)
v.dedup()retirer les doublons consécutifs
v.retain(|x| *x > 0)ne garder que ce qui vérifie la condition
v.extend(autre)ajouter tous les éléments
v.truncate(n) · v.clear()raccourcir · vider
v.reverse() · v.swap(i, j)inverser · échanger, sur place
v.binary_search(&x)vecteur trié → Ok(rang) ou Err(rang d’insertion)
v.chunks(3) · v.windows(2)paquets de 3 · fenêtres glissantes
v.split_at(i)deux tranches
v.join(", ")Vec de textes → un texte

String et &str

String::new() · String::from("a")créer ; ou "a".to_string()
s.push('c') · s.push_str("abc")ajouter
s1 + &s2 · format!("{s1}{s2}")concaténer (+ consomme s1)
s.len()! longueur en octets : "é".len() → 2
s.chars().count()nombre de caractères
s.chars() · s.bytes()parcourir caractères · octets
s.trim()sans espaces ni \n aux extrémités
s.split(',') · s.lines()découper (itérateurs)
s.split_whitespace()mots séparés par des blancs
s.split_once('=')→ Option<(&str, &str)>
s.contains("ab")aussi starts_with, ends_with
s.find("ab")position en octets → Option<usize>
s.replace("a", "b")remplacer → nouvelle String
s.to_uppercase()casse Unicode (to_lowercase)
s.repeat(3)répéter
s.parse::<f64>()convertir → Result
&s[0..3]! par octets : panique si un caractère est coupé

! s[0] est refusé : en UTF-8, un caractère occupe de 1 à 4 octets. Premiers caractères : s.chars().take(n).collect::<String>().

Autres collections (std::collections)

VecDeque<T>file à deux bouts : push_front, pop_front
HashMap<K, V>dictionnaire, ordre quelconque
BTreeMap<K, V>dictionnaire trié par clé
HashSet<T> · BTreeSet<T>ensemble sans doublon : union, intersection
BinaryHeap<T>file de priorité, plus grand en tête ; Reverse(x) : plus petit

Clés Hash… : Eq + Hash ; clés BTree… : Ord.

HashMap

use std::collections::HashMap;

let mut notes: HashMap<String, u32> = HashMap::new();
notes.insert("Anne".to_string(), 15);
if let Some(n) = notes.get("Anne") { println!("{n}"); }

let mut compte = HashMap::new();     // compter les mots
for mot in texte.split_whitespace() {
    *compte.entry(mot).or_insert(0) += 1;
}
for (cle, val) in &notes { println!("{cle} : {val}"); }
m.insert(k, v)ajoute ou remplace → ancienne valeur (Option)
m.get(&k) · m.get_mut(&k)→ Option
m.contains_key(&k) · m.remove(&k)présence · retrait
m.entry(k).or_insert(v)accès, en créant si absent
m.entry(k).or_default()idem, valeur par défaut
m.keys() · m.values()itérateurs

! Ordre de parcours imprévisible, variable d’une exécution à l’autre : BTreeMap, ou trier, si l’ordre compte.

Écrire son propre itérateur

struct Compte { n: u32 }
impl Iterator for Compte {
    type Item = u32;                 // type produit
    fn next(&mut self) -> Option<u32> {
        self.n += 1;
        if self.n <= 3 { Some(self.n) } else { None }
    }
}
// Compte { n: 0 }.collect::<Vec<_>>() → [1, 2, 3]

Seul next est à écrire : map, filter, sum… sont fournis par le trait.

Itérateurs

let v = vec![3, 1, 4, 1, 5, 9, 2, 6];
let doubles: Vec<i32> = v.iter()
    .filter(|x| *x % 2 == 0)         // 4, 2, 6
    .map(|x| x * 2)
    .collect();                      // [8, 4, 12]
let total: i32 = v.iter().sum();     // 31
Créer
v.iter() · v.iter_mut()éléments &T · &mut T
v.into_iter()consomme la collection → T
Adapter (paresseux)
map(f) · filter(p)transformer · garder si
filter_map(f)les deux ; f renvoie une Option
enumerate() · zip(b)→ (rang, x) · paires (a, b)
chain(b) · rev()à la suite · à l’envers
skip(n) · take(n)sauter · prendre n éléments
skip_while(p) · take_while(p)tant que la condition est vraie
step_by(n)un élément sur n
flat_map(f) · flatten()aplatir
copied() · cloned()&T → T
Consommer
collect()→ Vec, String, HashMap… (type à préciser)
sum() · product() · count()somme · produit · nombre
min() · max()→ Option ; aussi min_by_key(f), max_by_key(f)
fold(init, f) · reduce(f)accumuler
any(p) · all(p)au moins un · tous
find(p) · position(p)premier trouvé · son rang
partition(p)→ deux collections
for_each(f)action sur chaque élément

! Paresseux : sans consommateur (collect, sum, for…), rien n’est calculé.

7 · Projet : Cargo, modules, tests, documentation

Mémento — Rust

Structure d’un projet

mon_projet/
+-- Cargo.toml       manifeste : nom, dépendances
+-- Cargo.lock       versions exactes retenues (généré)
+-- src/
|   +-- main.rs      exécutable : fn main
|   +-- lib.rs       bibliothèque (facultatif)
|   +-- config.rs    module config
|   +-- reseau.rs    module reseau…
|   +-- reseau/
|       +-- client.rs  sous-module reseau::client
+-- tests/           tests d'intégration
+-- examples/        cargo run --example nom
+-- benches/         mesures de performance
+-- target/          produits de compilation (hors Git)

Cargo.toml

[package]
name = "mon_projet"
version = "0.1.0"
edition = "2024"

[dependencies]
regex = "1"                      # >= 1.0.0 et < 2.0.0
serde = { version = "1", features = ["derive"] }
outil = { path = "../outil" }    # dépendance locale

[dev-dependencies]               # pour les tests seulement
proptest = "1"

[profile.release]
lto = true                       # optimisation plus poussée

Versions : "1.2" accepte toute version compatible (≥ 1.2.0 et < 2.0.0) ; "=1.2.3" : cette version exacte.

Commandes Cargo

cargo new nom · cargo initcréer un projet · dans le dossier courant
cargo add regex · cargo remove regexajouter · retirer une dépendance
cargo add serde -F deriveavec une fonctionnalité (--features)
cargo build · --releasecompiler : debug · optimisé
cargo run -- arg1 arg2lancer avec des arguments
cargo test motiftests dont le nom contient le motif
cargo test -- --nocapturemontrer les println! des tests
cargo doc --opendocumentation du projet et des dépendances
cargo updateactualiser Cargo.lock (dans les limites de Cargo.toml)
cargo treearbre des dépendances
cargo install outilinstaller un programme depuis crates.io
cargo publish! publie sur crates.io ; une version publiée ne se remplace pas

! Une dépendance est du code tiers, exécuté dès la compilation (build.rs, macros) : vérifier auteur, licence, activité, avis de sécurité (rustsec.org ; outil tiers cargo audit).

Attributs utiles

#[derive(Debug, Clone)]implémentations automatiques
#[allow(dead_code)]taire un avertissement
#[cfg(test)]compilation conditionnelle (aussi target_os = "windows"…)
#[must_use]avertir si le résultat est ignoré
#![…]en tête de fichier : vaut pour tout le module

Modules et chemins

// src/main.rs
mod geometrie;                   // charge src/geometrie.rs
use geometrie::{distance, Point};
use std::io::{self, Write};      // io et io::Write

// src/geometrie.rs
pub struct Point { pub x: f64, pub y: f64 }

pub fn distance(a: &Point, b: &Point) -> f64 {
    (a.x - b.x).hypot(a.y - b.y)
}
mod m;déclarer un module : fichier m.rs (ou m/mod.rs)
mod m { … }module écrit sur place
use a::b::C;raccourcir un chemin
use a::b::C as D;importer sous un autre nom
use a::b::*;tout importer (à éviter, sauf tests)
crate:: · super:: · self::racine de la crate · module parent · module courant
pub use a::C;réexporter : façade publique

Crate : unité de compilation (un exécutable ou une bibliothèque). Paquet (package) : un Cargo.toml et ses crates.

Tests

pub fn diviser(a: i32, b: i32) -> i32 {
    if b == 0 { panic!("division par zéro"); }
    a / b
}

#[cfg(test)]                     // compilé pour cargo test
mod tests {
    use super::*;

    #[test]
    fn quotient() {
        assert_eq!(diviser(7, 2), 3);
        assert!(diviser(-6, 3) < 0, "négatif attendu");
    }

    #[test]
    #[should_panic(expected = "division par zéro")]
    fn par_zero() { diviser(1, 0); }

    #[test]
    fn lecture() -> Result<(), std::num::ParseIntError> {
        assert_eq!("42".parse::<i32>()?, 42);
        Ok(())
    }
}
assert_eq!(a, b) · assert_ne!égalité · différence ; message facultatif en plus
#[should_panic]le test doit paniquer
#[ignore]ignoré, sauf cargo test -- --ignored
tests/*.rstests d’intégration : API publique seulement

Documenter

/// Moyenne d'une tranche ; `None` si elle est vide.
///
/// ```
/// assert_eq!(mon_projet::moyenne(&[2.0, 4.0]), Some(3.0));
/// ```
pub fn moyenne(v: &[f64]) -> Option<f64> {
    if v.is_empty() { return None; }
    Some(v.iter().sum::<f64>() / v.len() as f64)
}

/// documente l’élément suivant (Markdown) ; //! le module entier. Les blocs de code de la documentation sont exécutés par cargo test (bibliothèques).

8 · Concurrence, idiomes, pièges, ressources

Mémento — Rust

Threads

use std::sync::{mpsc, Arc, Mutex};
use std::thread;

let h = thread::spawn(|| 6 * 7);
let r = h.join().unwrap();           // 42

let compteur = Arc::new(Mutex::new(0));
let mut poignees = vec![];
for _ in 0..4 {
    let c = Arc::clone(&compteur);
    poignees.push(thread::spawn(move || {
        *c.lock().unwrap() += 1;
    }));
}
for p in poignees { p.join().unwrap(); }
// *compteur.lock().unwrap() vaut 4

let (tx, rx) = mpsc::channel();      // canal
thread::spawn(move || tx.send("message").unwrap());
println!("{}", rx.recv().unwrap());

let v = vec![1, 2, 3];
thread::scope(|s| {                  // threads bornés :
    s.spawn(|| println!("{v:?}"));   // emprunt permis
});
  • Send : transférable à un autre thread · Sync : partageable par référence. Rc ou RefCell entre threads : refusé (E0277).
  • lock() renvoie Err si un thread a paniqué en tenant le verrou (verrou « empoisonné »).
  • ! Le verrou reste pris jusqu’à la fin de la portée du garde : sections courtes. Deux verrous pris dans des ordres différents → interblocage, que le compilateur ne détecte pas.

Asynchrone : async / await

  • async fn renvoie un futur : rien ne s’exécute avant .await.
  • La bibliothèque standard ne fournit pas d’exécuteur : il vient d’une crate tierce, le plus souvent tokio.
  • Pour de nombreuses attentes d’entrées-sorties (réseau, serveurs). Pour du calcul : threads.
#[tokio::main]
async fn main() {
    let (a, b) = tokio::join!(tache_a(), tache_b());
}

Macros et unsafe

macro_rules! carre {
    ($x:expr) => { $x * $x };
}
let n = carre!(3 + 1);           // 16 : $x reste groupé

Un bloc unsafe { } autorise cinq opérations invérifiables : déréférencer un pointeur brut, appeler une fonction unsafe (dont le code C), lire ou modifier un static mut, implémenter un trait unsafe, lire un champ d’union. Les autres règles restent actives. Un programme d’application n’en a presque jamais besoin.

Boîte à outils de la bibliothèque standard

mem::swap(&mut a, &mut b)échanger deux valeurs
mem::take(&mut v)extraire, en laissant la valeur par défaut
mem::replace(&mut v, n)remplacer, en récupérant l’ancienne
a.cmp(&b)→ Ordering::Less, Equal ou Greater
Instant::now() · t.elapsed()mesurer une durée (std::time)
Command::new("git").arg("status").output()?lancer un programme (std::process)
c.is_alphabetic() · c.is_ascii_digit()nature d’un caractère
c.to_digit(10) · char::from_digit(7, 10)chiffre ↔ caractère

Idiomes

  • Constructeur new() et impl Default ; impl From<X> for Y pour convertir (utilisé par ?).
  • Paramètres &str et &[T] ; valeurs renvoyées possédées.
  • Itérateurs plutôt que boucles sur des indices.
  • « Newtype » struct Euros(f64); : ne plus confondre deux unités.
  • Rendre les états impossibles irreprésentables : une enum plutôt que des booléens combinés.
  • Option pour l’absence, jamais de valeur sentinelle (-1, "").

Pièges fréquents

.clone() pour faire taire le compilateurrevoir qui possède quoi ; emprunter
.unwrap() partout?, expect, traitement explicite
s.len() pris pour des caractèress.chars().count()
&s[..n] sur un texte accentuépanique : s.chars().take(n)
dépassement d’entier silencieux en --releasechecked_add… ; tester en debug
v.sort() sur des f64refusé : trier avec total_cmp (page 4)
itérateur sans consommateurrien n’est calculé
modifier une collection en la parcourantrefusé : retain, ou collecter d’abord
performances mesurées en debugtoujours --release

Outils autour du compilateur

  • rust-analyzer : extension d’éditeur (VS Code…) — complétion, types affichés, erreurs en direct. cargo fmt et cargo clippy avant chaque commit.
  • cargo clippy --fix : appliquer les corrections proposées.

De Python à Rust

PythonRust
NoneOption<T> : None, Some(x)
try / exceptResult<T, E>, ?, match
list · dict · setVec · HashMap · HashSet
for i in range(n)for i in 0..n
len(s)s.chars().count() (s.len() : octets)
f"{x}"format!("{x}")
class · héritagestruct + impl · traits et composition
lambda x: x + 1|x| x + 1
pip · pytestcargo add · cargo test

Glossaire français – anglais

possession ownershipemprunt borrowvérificateur d’emprunts borrow checkerdurée de vie lifetimelibération droptranche slicefermeture closurefiltrage par motif pattern matchingdéplacement movepointeur intelligent smart pointermasquage shadowingmutabilité intérieure interior mutability

Pour aller plus loin

  • Le langage de programmation Rust (traduction du « Book ») : jimskapt.github.io/rust-book-fr
  • Comprehensive Rust (Google), en français : google.github.io/comprehensive-rust/fr
  • Rust by Example : doc.rust-lang.org/rust-by-example
  • Rustlings, exercices guidés par le compilateur : github.com/rust-lang/rustlings
  • Bibliothèque standard : doc.rust-lang.org/std · paquets : crates.io, docs.rs
  • Essayer sans rien installer : play.rust-lang.org