Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Le modèle de propriété (ownership)

Le système de propriété (ownership) est l’innovation majeure de Rust. Il permet de garantir la sécurité de la mémoire (pas de pointeurs orphelins, pas de double libération) sans avoir recours à un ramasse-miettes (garbage collector) ni nécessiter de gestion manuelle de la mémoire de la part du développeur.

Les trois règles d’or

La gestion de la mémoire en Rust repose sur trois règles strictes appliquées par le compilateur :

  1. Chaque valeur a un propriétaire (une variable appelée son owner).
  2. Il ne peut y avoir qu’un seul propriétaire à la fois.
  3. Lorsque le propriétaire sort du bloc de portée (scope), la valeur est automatiquement détruite.

La portée d’une variable (Scope)

La portée est la zone du programme dans laquelle un élément est valide. Elle est généralement délimitée par des accolades {}.

fn main() {
{                      // `s` n'est pas valide ici, elle n'est pas encore déclarée
    let s = "texte";   // `s` est valide à partir d'ici
    // on utilise s
}                      // la portée prend fin, `s` n'est plus valide
// println!("{s}") // Génère une erreur de compilation
}

Lorsque la variable sort de sa portée, Rust appelle automatiquement une fonction spéciale nommée drop qui restitue la mémoire au système.

Sémantique de déplacement (Move)

Pour comprendre l’ownership, il faut distinguer deux espaces mémoire :

  • la pile (stack) et
  • le tas (heap).

Prenons l’exemple du type String, dont les données textuelles sont allouées sur le tas.

let s1 = String::from("hello");
let s2 = s1;

En Rust, pour éviter de copier des données potentiellement lourdes sur le tas ou de créer deux pointeurs vers la même zone mémoire (ce qui provoquerait une erreur de double libération à la fin du bloc), la propriété de la donnée est transférée de s1 à s2.

On dit que s1 a été déplacée (moved).

Si nous tentons d’ajouter ce code à celui qui précède, nous obtenons une erreur à la compilation :

println!("{s1}"); // ERREUR DE COMPILATION ! s1 n'est plus valide.

Pour vérifier :

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    // println!("{s1}"); // ERREUR DE COMPILATION ! s1 n'est plus valide.
}

Sémantique de copie (Trait Copy)

Certains types font exception à cette règle de déplacement. Ce sont les types dont la taille est connue à la compilation et qui sont entièrement stockés sur la pile. Ils implémentent le trait Copy.

fn main() {
    let x = 5;
    let y = x; // Copie bit à bit sur la pile

println!("x = {x}, y = {y}"); // Valide ! x n'a pas été déplacé.
}
  • Types qui copient : Les entiers (i32, u64…), les booléens, les flottants, les caractères (char) et les tuples ne contenant que des types Copy.
  • Types qui déplacent : String, Vec, les structures personnalisées (par défaut).

Ownership et fonctions

Le passage d’une valeur à une fonction suit exactement les mêmes règles sémantiques que l’assignation à une variable : passer une variable à une fonction va soit la déplacer, soit la copier.

fn main() {
    let s = String::from("Rust"); 
    prendre_possession(s);         // `s` est déplacée dans la fonction
    // println!("{s}");            // ERREUR ! `s` n'est plus utilisable ici

    let x = 42;
    faire_une_copie(x);            // `x` est copiée
    println!("{x}");               // Valide ! `x` est toujours utilisable
}

fn prendre_possession(texte: String) {
    println!("{texte}");
} // `drop` est appelé ici pour `texte`, la mémoire est libérée.

fn faire_une_copie(nombre: i32) {
    println!("{nombre}");
}

Pour éviter de perdre la propriété d’un objet à chaque appel de fonction, Rust introduit les concepts de références et d’emprunts (references & borrowing), qui permettent d’utiliser une valeur sans en devenir propriétaire.