L'ownership en Rust
L’ownership (possession) est l’idée qui fait de Rust ce qu’il est. C’est grâce à elle que le langage garantit une utilisation sûre de la mémoire sans ramasse-miettes (garbage collector) et sans que vous ayez à libérer la mémoire vous-même. Au début, cela paraît inhabituel, mais l’idée de base est simple. Construisons-la pas à pas.
Le problème que résout l’ownership
Section intitulée « Le problème que résout l’ownership »Quand un programme tourne, il range des données en mémoire. À un moment donné, cette mémoire doit être libérée ; sinon, le programme consomme de plus en plus de mémoire (on parle de fuite de mémoire) et finit par planter. Les langages règlent ce problème de deux façons courantes :
- À la main (C/C++) : c’est vous qui libérez la mémoire. Si vous oubliez, la mémoire fuit. Si vous la libérez trop tôt, vous obtenez des plantages et des failles de sécurité.
- Avec un ramasse-miettes (Java, Go, Python) : un processus en arrière-plan fait le ménage pour vous. C’est pratique, mais cela coûte en performances et rend le programme moins prévisible.
Rust choisit une troisième voie : il décide dès la compilation du moment exact où chaque valeur doit être libérée, en suivant quelques règles d’ownership. Pas de libération manuelle, pas de ramasse-miettes.
Les trois règles de l’ownership
Section intitulée « Les trois règles de l’ownership »Tout le système tient dans ces trois règles. Le reste en découle :
- Chaque valeur a un seul propriétaire (une variable qui la « possède »).
- Il ne peut y avoir qu’un seul propriétaire à la fois.
- Quand le propriétaire sort de sa portée, la valeur est automatiquement libérée.
Voyons chaque règle en action.
Règle 3 : les valeurs sont libérées à la fin de leur portée
Section intitulée « Règle 3 : les valeurs sont libérées à la fin de leur portée »Une portée (scope), c’est la zone comprise entre { et }. Quand la portée d’une variable se termine, Rust libère automatiquement sa valeur. Vous n’écrivez jamais free() :
fn main() { { let message = String::from("bonjour"); // message est valide ici println!("{message}"); } // <- fin de la portée : `message` est libéré automatiquement
// println!("{message}"); // ❌ erreur : message n'existe plus ici}Vous n’avez rien eu à libérer : Rust s’en est chargé au moment précis où message est sorti de sa portée.
Règles 1 et 2 : le « move »
Section intitulée « Règles 1 et 2 : le « move » »C’est ici que les choses deviennent intéressantes. Regardez ce qui se passe quand vous affectez une variable à une autre :
fn main() { let s1 = String::from("bonjour"); let s2 = s1; // l'ownership PASSE de s1 à s2
println!("{s2}"); // ✅ fonctionne // println!("{s1}"); // ❌ erreur : s1 a été déplacé}Quand vous écrivez let s2 = s1, Rust ne copie pas la chaîne : il déplace (move) l’ownership de s1 vers s2. Comme la règle 2 n’autorise qu’un seul propriétaire, s1 n’est plus valide. Si vous essayez de l’utiliser, vous obtenez :
error[E0382]: borrow of moved value: `s1`Autrement dit (rustc s’exprime toujours en anglais) : vous essayez d’utiliser s1 alors que sa valeur a déjà été déplacée.
Et si je veux deux copies ?
Section intitulée « Et si je veux deux copies ? »Si vous voulez vraiment une deuxième copie, indépendante de la première, appelez .clone() :
fn main() { let s1 = String::from("bonjour"); let s2 = s1.clone(); // fait une copie complète
println!("s1 = {s1}, s2 = {s2}"); // ✅ les deux fonctionnent maintenant}clone() est explicite exprès : une copie peut coûter cher, alors Rust vous oblige à la demander.
Les types simples sont copiés, pas déplacés
Section intitulée « Les types simples sont copiés, pas déplacés »Vous vous demandez peut-être pourquoi ce code-là ne provoque pas d’erreur :
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ les deux fonctionnent}Les petites valeurs de taille fixe, comme les entiers, les nombres à virgule, les booléens et les char, ne coûtent presque rien à copier. Rust les copie donc au lieu de les déplacer. Pour débuter, retenez cette règle simple :
- Valeurs simples (nombres,
bool,char) → copiées. L’original reste valide. - Données possédées (comme une
String) → déplacées. L’original devient invalide.
L’ownership et les fonctions
Section intitulée « L’ownership et les fonctions »Passer une valeur à une fonction la déplace aussi, exactement comme l’affecter à une variable :
fn take_ownership(s: String) { println!("Maintenant, je possède : {s}");} // s est libéré ici
fn main() { let text = String::from("bonjour"); take_ownership(text); // println!("{text}"); // ❌ erreur : text a été déplacé dans la fonction}C’est justement pour cela que le sujet suivant, le borrowing (emprunt), existe. Le borrowing permet à une fonction d’utiliser une valeur sans en prendre l’ownership, pour que vous puissiez continuer à vous en servir ensuite.
Exercices
Section intitulée « Exercices »Essayez d’abord chaque exercice vous-même, dans le Rust Playground ou dans un projet local. Vous rencontrerez sans doute des erreurs de compilation en chemin : c’est normal. Lisez-les, elles pointent généralement droit sur le move.
1. Garder l’original
Section intitulée « 1. Garder l’original »Ce programme ne compile pas. Corrigez-le pour qu’il affiche brouillon -> brouillon v2, sans supprimer aucune des deux variables.
fn main() { let original = String::from("brouillon"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("brouillon"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveLe compilateur vous indique que original a été déplacé à la ligne 3 (« value moved here »), puis que vous essayez de l’utiliser à la ligne 5.
Indice : il vous faut deux chaînes indépendantes, pour que modifier l’une ne touche pas l’autre.
Voir la solution
fn main() { let original = String::from("brouillon"); let mut edited = original.clone(); // une copie complète et séparée edited.push_str(" v2"); println!("{original} -> {edited}");}Sortie :
brouillon -> brouillon v2let mut edited = original; déplace la chaîne, donc original n’est plus valide. .clone() crée une deuxième chaîne, avec sa propre mémoire. Désormais, push_str ne modifie que edited, et original contient toujours brouillon.
2. Rendre la valeur
Section intitulée « 2. Rendre la valeur »Écrivez une fonction add_signature qui prend l’ownership d’une String, ajoute " -- Ferris" à la fin, puis rend la chaîne. Appelez-la depuis main et affichez le résultat.
Indice : une fonction peut renvoyer la valeur qu’elle a reçue. Écrivez -> String dans la signature, et déclarez le paramètre mut text: String pour avoir le droit de le modifier.
Voir la solution
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // l'ownership repart vers l'appelant}
fn main() { let letter = String::from("À bientôt !"); let letter = add_signature(letter); println!("{letter}");}Sortie :
À bientôt ! -- FerrisL’ownership entre dans la fonction au moment de l’appel, puis ressort avec la valeur de retour. On la récupère dans une nouvelle variable letter, qui masque l’ancienne (c’est le shadowing). Cela fonctionne, mais faire entrer et sortir les valeurs sans arrêt devient vite fatigant. C’est exactement le problème que résout le borrowing.
3. À qui appartient-elle maintenant ?
Section intitulée « 3. À qui appartient-elle maintenant ? »Ce programme ne compile pas. Corrigez-le pour qu’il affiche Ana a rejoint la liste, puis ["Ana"].
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name} a rejoint la liste"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name} a rejoint la liste"); | ^^^^ value borrowed here after moveIndice : push prend l’ownership, exactement comme un appel de fonction. Qui possède la chaîne après la ligne 4 ? Soit vous utilisez name avant le move, soit vous demandez au nouveau propriétaire.
Voir la solution
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name} a rejoint la liste"); // on l'utilise tant qu'on la possède names.push(name); // maintenant, c'est le vecteur qui la possède println!("{:?}", names);}Sortie :
Ana a rejoint la liste["Ana"]Après names.push(name), c’est le vecteur qui est propriétaire, et name n’est plus utilisable. Afficher le message d’abord est une solution. Une autre consiste à garder l’ordre d’origine et à afficher names[0] à la place, puisque le vecteur contient toujours la chaîne. Un clone fonctionnerait aussi, mais ici, inutile de copier quoi que ce soit.
Récapitulatif
Section intitulée « Récapitulatif »- L’ownership permet à Rust de gérer la mémoire en toute sécurité, sans ramasse-miettes.
- Trois règles : un propriétaire par valeur, un seul propriétaire à la fois, et la valeur est libérée quand son propriétaire sort de sa portée.
- Affecter des données possédées (comme une
String) les déplace : l’original devient invalide. - Utilisez
.clone()pour obtenir une copie indépendante ; les types simples (nombres,bool,char) sont copiés automatiquement. - Passer une valeur à une fonction la déplace aussi, ce qui nous amène au borrowing.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. En Rust, combien de propriétaires une valeur peut-elle avoir à un instant donné ?
2. Quelle méthode appelez-vous pour faire une copie indépendante d'une String au lieu de la déplacer ?
3. Lequel de ces types est copié automatiquement, au lieu d'être déplacé, quand on l'affecte à une autre variable ?
4. Que se passe-t-il quand vous passez une String à une fonction qui la prend par valeur (et non par référence) ?
5. D'après cette page, quand Rust libère-t-il automatiquement une valeur ?
Score : 0 / 5
Pour aller plus loin
Section intitulée « Pour aller plus loin »- What Is Ownership? — The Rust Book (en anglais) : le chapitre officiel, avec des schémas montrant à quoi ressemble une
Stringen mémoire et pourquoi un move ne la copie pas. - Ownership and moves — Rust by Example (en anglais) : de courts exemples exécutables de moves, y compris vers des fonctions.
- RAII — Rust by Example (en anglais) : comment les valeurs sont libérées quand elles sortent de leur portée, et le rôle du trait
Drop. Copy— documentation de la bibliothèque standard (en anglais) : quels types sont copiés plutôt que déplacés, et la différence entreCopyetClone.- Rust track — Exercism (en anglais) : des exercices gratuits avec des retours sur vos solutions, pour quand vous voudrez vous entraîner davantage.