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 3·35 min

Ownership et emprunts

L'ownership (la propriété) est la règle qui permet à Rust de gérer la mémoire sans ramasse-miettes et sans free() manuel. Chaque valeur a un propriétaire unique ; quand le propriétaire sort de portée, la valeur est libérée. Les emprunts permettent de prêter une valeur sans la donner. Tout le reste du langage découle de ces deux idées. C'est le chapitre à relire deux fois.

Pile et tas, en deux phrases

Les valeurs de taille fixe connue à la compilation (un i32, un bool, un tableau [u8; 4]) vivent sur la pile : rapide, libérée automatiquement à la fin de la fonction. Les valeurs dont la taille varie (une String, un Vec) stockent leur contenu sur le tas, et gardent sur la pile un petit en-tête : un pointeur, une longueur, une capacité.

La question que résout l'ownership : qui libère le contenu du tas, et quand ?

Les trois règles

  1. Chaque valeur a un propriétaire, une variable.
  2. Il n'y a qu'un seul propriétaire à la fois.
  3. Quand le propriétaire sort de portée, la valeur est libérée.
fn main() {
    {
        let s = String::from("bonjour"); // s est propriétaire
        println!("{s}");
    } // fin de portée : la mémoire de s est libérée ici
}

Pas de ramasse-miettes qui passe plus tard, pas d'oubli possible : la libération est insérée par le compilateur à l'accolade fermante.

Le déplacement (move)

C'est ici que Rust surprend. Une affectation ne copie pas une String, elle la déplace :

fn main() {
    let a = String::from("bonjour");
    let b = a;              // la propriété passe de a à b
    // println!("{a}");     // erreur : a a été déplacée
    println!("{b}");
}
error[E0382]: borrow of moved value: `a`

Pourquoi ? Si a et b pointaient tous deux sur le même texte, les deux seraient libérés en fin de portée : une double libération, le bug classique du C. Rust l'interdit en rendant a inutilisable après le déplacement.

Passer une valeur à une fonction la déplace aussi :

fn afficher(s: String) {
    println!("{s}");
} // s est libérée ici
 
fn main() {
    let message = String::from("salut");
    afficher(message);
    // afficher(message); // erreur : message a déjà été déplacé
}

Copier explicitement : clone

Si vous voulez vraiment deux exemplaires, demandez-le :

fn main() {
    let a = String::from("bonjour");
    let b = a.clone();      // copie du contenu du tas
    println!("{a} {b}");
}

clone a un coût, et il se voit dans le code. C'est voulu.

Les types Copy

Les entiers, flottants, booléens, char et les tuples de ces types ne sont pas déplacés mais copiés, car leur copie ne coûte rien :

fn main() {
    let x = 5;
    let y = x;
    println!("{x} {y}"); // OK : i32 est Copy
}

Les emprunts : prêter sans donner

Déplacer une valeur dans chaque fonction qui la lit serait absurde. On la prête avec &, qui crée une référence :

fn longueur(s: &String) -> usize {
    s.len()
} // s est une référence : rien n'est libéré
 
fn main() {
    let texte = String::from("bonjour");
    let n = longueur(&texte);
    println!("{texte} fait {n} octets"); // texte est toujours utilisable
}

Une référence permet de lire, pas de modifier. Pour modifier, on emprunte en mutable avec &mut :

fn ajouter_point(s: &mut String) {
    s.push('!');
}
 
fn main() {
    let mut texte = String::from("bonjour");
    ajouter_point(&mut texte);
    println!("{texte}"); // bonjour!
}

La règle des emprunts

À un instant donné, on peut avoir :

  • soit autant de références & qu'on veut,
  • soit une seule référence &mut,
  • jamais les deux en même temps.
fn main() {
    let mut v = vec![1, 2, 3];
    let premier = &v[0];     // emprunt en lecture
    v.push(4);               // erreur : emprunt mutable pendant la lecture
    println!("{premier}");
}

Ce code semble anodin. Il est pourtant dangereux : push peut réallouer le vecteur ailleurs en mémoire, et premier pointerait alors vers une zone libérée. En C++, ce bug compile et plante au hasard. En Rust, il ne compile pas.

La règle s'applique jusqu'à la dernière utilisation de la référence, pas jusqu'à la fin du bloc. Ce code-ci compile :

fn main() {
    let mut v = vec![1, 2, 3];
    let premier = &v[0];
    println!("{premier}");   // dernière utilisation de premier
    v.push(4);               // OK : plus aucun emprunt actif
    println!("{v:?}");
}

Cette règle (plusieurs lecteurs ou un seul écrivain) est aussi ce qui rend la concurrence sûre au chapitre 10. Deux threads ne peuvent pas modifier la même donnée sans synchronisation, parce que le compilateur l'interdit.

Pas de référence pendante

Rust refuse de renvoyer une référence vers une valeur qui va disparaître :

fn creer() -> &String {
    let s = String::from("temporaire");
    &s
} // s est libérée ici, la référence pointerait dans le vide

Le compilateur répond par une erreur sur la durée de vie, et suggère de renvoyer la String elle-même. Les durées de vie font l'objet du chapitre 7.

String et &str

Deux types de texte coexistent, et c'est la première source de confusion :

TypeCe que c'estPropriétaire ?
StringTexte modifiable, alloué sur le tasOui
&strVue en lecture sur du texte existant (une « tranche »)Non, c'est un emprunt

Un littéral "bonjour" est un &str : une vue sur du texte gravé dans l'exécutable. &ma_string ou &ma_string[0..3] donnent aussi des &str.

La règle pratique : une fonction qui lit du texte prend un &str. Elle accepte alors un littéral comme une String empruntée.

fn premier_mot(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}
 
fn main() {
    let phrase = String::from("Rust est rapide");
    println!("{}", premier_mot(&phrase)); // une String empruntée
    println!("{}", premier_mot("bonjour le monde")); // un littéral
}

Les tranches existent aussi pour les tableaux et les vecteurs : &[i32] est une vue sur une suite d'entiers, que ce soit un tableau ou un Vec. Même règle : une fonction qui lit une liste prend &[T], pas &Vec<T>.

Attention, les chaînes Rust sont en UTF-8. &s[0..1] sur "école" panique, car é occupe deux octets et on couperait au milieu. Pour parcourir des caractères, utilisez s.chars().

Comment ne plus se battre contre le compilateur

Les premières semaines, l'erreur « borrow of moved value » revient sans arrêt. Voici l'ordre dans lequel je pose les questions :

  1. La fonction a-t-elle besoin de posséder la valeur ? Si elle ne fait que lire, elle prend &T. Si elle modifie, &mut T. Elle ne prend T que si elle doit garder ou détruire la valeur.
  2. Est-ce que je garde une référence trop longtemps ? Raccourcissez sa portée, ou récupérez ce dont vous avez besoin (un index, une copie d'un nombre) avant de modifier.
  3. Un clone est-il acceptable ? Sur une petite chaîne, hors d'une boucle critique, oui. Cloner pour avancer, puis optimiser plus tard, est une stratégie légitime pour débuter.

Exercice

Ce programme ne compile pas. Corrigez-le sans utiliser clone.

fn compter_voyelles(s: String) -> usize {
    s.chars().filter(|c| "aeiouy".contains(*c)).count()
}
 
fn main() {
    let mot = String::from("ownership");
    let n = compter_voyelles(mot);
    println!("{mot} contient {n} voyelles");
}

Solution : la fonction n'a besoin que de lire. Elle prend un &str, et on lui prête le mot.

fn compter_voyelles(s: &str) -> usize {
    s.chars().filter(|c| "aeiouy".contains(*c)).count()
}
 
fn main() {
    let mot = String::from("ownership");
    let n = compter_voyelles(&mot);
    println!("{mot} contient {n} voyelles");
}

Un propriétaire, des emprunts en lecture multiples ou un seul en écriture, et une libération automatique en fin de portée. Ce modèle demande un temps d'adaptation, puis il disparaît : on finit par penser en termes de « qui possède cette donnée ? », et le code s'en trouve plus clair. Le chapitre suivant construit des types à soi avec les structs et les enums.

Précédent
Variables, types, fonctions et contrôle de flux
Suivant
Structs, enums et pattern matching

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