backend / rust
Ownership : le concept central
Explication
Ce que vous allez apprendre
- Comprendre les trois règles de l'ownership qui fondent la sécurité mémoire de Rust
- Distinguer un move d'une copie selon le type de la donnée
- Utiliser
.clone()pour dupliquer explicitement une donnée sur le heap - Comprendre ce qui se passe quand une valeur est passée à une fonction
- Voir le drop automatique en fin de scope comme un remplaçant du garbage collector
Dans quel contexte ?
Un développeur qui vient de C++ a l'habitude de se méfier des fuites mémoire (oublier de libérer un malloc) et des double-free (libérer deux fois la même zone mémoire). Il découvre que Rust élimine structurellement ces deux catégories de bugs à la compilation, sans jamais introduire de garbage collector qui tournerait à l'exécution et grèverait les performances. C'est exactement le rôle de l'ownership : la véritable innovation de Rust par rapport à tous les langages qui l'ont précédé.
D'abord, il faut mémoriser trois règles simples mais qui gouvernent tout le reste
Chaque valeur en Rust a un seul propriétaire à un instant donné. Quand ce propriétaire sort de sa portée (scope), la valeur est automatiquement libérée. Et il ne peut jamais y avoir deux propriétaires simultanés d'une même donnée.
Ces trois règles, appliquées rigoureusement par le compilateur, remplacent entièrement le besoin d'un garbage collector : le compilateur sait, dès la compilation, exactement à quel moment précis chaque donnée doit être libérée.
Une fois ces règles posées, une conséquence surprenante apparaît
let s1 = String::from("hello"); let s2 = s1; ne copie PAS la donnée : la propriété de la chaîne "hello" est transférée de s1 à s2 (un "move"). Après cette ligne, s1 devient invalide et toute tentative de l'utiliser produit une erreur de compilation — pas une erreur à l'exécution, une erreur détectée avant même que le programme ne tourne.
| Situation | Comportement | Pourquoi |
|---|---|---|
let s2 = s1; (String) | Move : s1 devient invalide | Éviter deux propriétaires de la même donnée heap |
let y = x; (i32) | Copie : x reste valide | Type Copy, bon marché à dupliquer |
let s2 = s1.clone(); | Copie explicite et coûteuse | Duplication réelle et volontaire des données heap |
Prérequis
Il faut être à l'aise avec les variables et les types de base (leçons précédentes). Cette leçon est le concept le plus important de tout le cours Rust : prends le temps de bien l'assimiler avant de continuer.
Il reste une exception importante à cette règle de move
Les types simples qui tiennent entièrement sur la pile (entiers, booléens, char, et les tuples composés uniquement de tels types) implémentent le trait Copy. Pour eux, let y = x; copie réellement la valeur au lieu de la déplacer, et x reste parfaitement valide après. C'est parce que copier un entier de 8 octets ne coûte rien, contrairement à copier une String qui pourrait faire des mégaoctets.
Piège fréquent
Passer une String (ou tout type non-Copy) à une fonction par valeur en transfère l'ownership : la variable d'origine devient inutilisable après l'appel, même si la fonction ne fait "que" la lire. C'est un piège fréquent au début — la solution, vue dans la prochaine leçon, consiste à passer une référence plutôt que la valeur elle-même.
Enfin, le drop automatique élimine toute une classe de bugs
Bonne pratique
Fais confiance au drop automatique en fin de bloc (RAII) plutôt que de chercher un équivalent manuel de free() ou delete : Rust garantit qu'une ressource (mémoire, fichier ouvert, connexion) sera libérée exactement une fois, au bon moment, sans jamais avoir à l'écrire explicitement dans la majorité des cas.
Maintenant que tu comprends le transfert de propriété, la prochaine leçon montre comment utiliser une valeur SANS en prendre l'ownership : le borrowing, la clé pour écrire du code Rust fluide au quotidien.
Commandes & code
Ownership : le concept central
L'ownership est la règle qui permet à Rust de garantir la sécurité mémoire sans garbage collector.
fn main() {
// Regle 1 : chaque valeur a UN SEUL proprietaire
// Regle 2 : quand le proprietaire sort du scope, la valeur est liberee (drop)
// Regle 3 : il ne peut y avoir qu'un seul proprietaire a la fois
let s1 = String::from("hello"); // s1 possede la donnee heap "hello"
let s2 = s1; // MOVE : la propriete passe a s2, s1 devient invalide
// println!("{}", s1); // ERREUR: value borrowed after move
println!("{}", s2); // OK
// Les types "Copy" (entiers, bool, char, tuples de Copy...) ne bougent pas, ils sont copies
let x = 5;
let y = x; // COPIE, pas move : x reste valide
println!("{} {}", x, y);
// clone() : copie explicite et couteuse des donnees heap
let s3 = String::from("world");
let s4 = s3.clone(); // duplique reellement la memoire heap
println!("{} {}", s3, s4); // les deux restent valides
// Ownership et fonctions : passer une valeur par valeur = move
fn prend_ownership(s: String) {
println!("possede maintenant: {}", s);
} // s est droppe ici, la memoire est liberee
let s5 = String::from("temporaire");
prend_ownership(s5);
// println!("{}", s5); // ERREUR: s5 a ete deplace dans la fonction
// Retourner l'ownership pour "recuperer" une valeur
fn prend_et_rend(s: String) -> String {
println!("traitement de: {}", s);
s // retourne l'ownership a l'appelant
}
let s6 = String::from("aller-retour");
let s6 = prend_et_rend(s6);
println!("{}", s6);
// Scope et drop automatique (RAII, comme en C++)
{
let s7 = String::from("temporaire dans un bloc");
println!("{}", s7);
} // s7.drop() appele automatiquement ici, memoire liberee immediatement
}Résumé
- Chaque valeur a un unique propriétaire ; assigner une valeur non-
Copyla déplace (move). - Après un move, l'ancienne variable devient invalide — le compilateur l'interdit à la compilation.
.clone()duplique explicitement la donnée quand un vrai partage indépendant est voulu.- Le drop automatique en fin de scope (RAII) élimine toute la classe des fuites mémoire et double-free.
Exercices pratiques
Mission : reparer une pipeline de traitement de commandes
Objectif : Diagnostiquer une chaine d'erreurs de move dans un pipeline de fonctions et proposer une correction coherente avec l'ownership.
Contexte
Un dev junior a ecrit ce pipeline de traitement de commandes, qui refuse de compiler avec plusieurs erreurs 'value borrowed after move' en cascade :
fn journaliser(commande: String) {
println!("log: {}", commande);
}
fn facturer(commande: String) -> String {
format!("facture pour {}", commande)
}
fn main() {
let commande = String::from("commande-42");
journaliser(commande);
let facture = facturer(commande);
println!("{}", facture);
}Il te demande de l'aider a comprendre pourquoi, et de proposer la version corrigee la plus idiomatique.