Retour au cours

backend / rust

Ownership : le concept central

Leçon 41 exercice

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.

SituationComportementPourquoi
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 valideType Copy, bon marché à dupliquer
let s2 = s1.clone();Copie explicite et coûteuseDuplication 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.

rust
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-Copy la 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

1 disponible
1

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 :

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

Résoudre l’exercice →