Aller au contenu

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.

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.

Tout le système tient dans ces trois règles. Le reste en découle :

  1. Chaque valeur a un seul propriétaire (une variable qui la « possède »).
  2. Il ne peut y avoir qu’un seul propriétaire à la fois.
  3. 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
}
▶ Essayez par vous-même

Vous n’avez rien eu à libérer : Rust s’en est chargé au moment précis où message est sorti de sa portée.

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é
}
▶ Essayez par vous-même

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.

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
}
▶ Essayez par vous-même

clone() est explicite exprès : une copie peut coûter cher, alors Rust vous oblige à la demander.

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.

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.

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.

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` trait
3 | let mut edited = original;
| -------- value moved here
4 | edited.push_str(" v2");
5 | println!("{original} -> {edited}");
| ^^^^^^^^ value borrowed here after move

Le 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 v2
▶ Essayez par vous-même

let 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.

É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 ! -- Ferris
▶ Essayez par vous-même

L’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.

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` trait
4 | names.push(name);
| ---- value moved here
5 | println!("{name} a rejoint la liste");
| ^^^^ value borrowed here after move

Indice : 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"]
▶ Essayez par vous-même

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.

  • 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

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