1. Ownership, Borrowing, Lifetimes

Le système d’ownership est ce qui rend Rust unique. Il garantit la sécurité mémoire sans garbage collector. Comprendre l’ownership est la clé de tout le reste.


1.1 Ownership — Règles Fondamentales

Chaque valeur en Rust a un propriétaire unique. Trois règles :

  1. Chaque valeur a exactement un propriétaire à la fois.
  2. Quand le propriétaire sort de portée, la valeur est libérée (drop).
  3. La propriété peut être transférée (move) ou empruntée (borrow).
fn main() {
    let s1 = String::from("hello"); // s1 est propriétaire
    let s2 = s1;                     // MOVE : s1 n'est plus valide
    // println!("{}", s1);           // ⛔ ERREUR : s1 a été moved
    println!("{}", s2);              // ✅ s2 est le nouveau propriétaire
}

Move vs Copy

TypeComportementExemples
CopyDupliqué implicitement (pas de move)scalaires : i32, f64, bool, char, tuples de Copy
MoveTransféré, l’original n’est plus valideString, Vec, pointeurs, struct sans #[derive(Copy)]
let x = 42;          // i32 implémente Copy
let y = x;           // copie, x reste valide
println!("{x} {y}");  // ✅
 
let s = String::from("hello");  // String n'est pas Copy
let t = s;                       // move !
// println!("{s}");              // ⛔

Portée et libération

{
    let s = String::from("hello");
    // s est valide ici
}  // ← s sort de portée, drop() libère la mémoire automatiquement

Fonctions et ownership

fn take_ownership(s: String) {      // prend ownership
    println!("{s}");
}  // ← drop ici
 
fn give_ownership() -> String {
    String::from("world")           // donne ownership à l'appelant
}
 
let s = give_ownership();
take_ownership(s);
// println!("{s}");  // ⛔ moved dans take_ownership

1.2 Borrowing — Emprunter sans posséder

Références immutables (&T)

On peut créer autant de références immutables qu’on veut. Elles ne permettent pas de modifier la valeur.

fn calculate_length(s: &String) -> usize {  // emprunte (borrow)
    s.len()
}  // ← pas de drop : le propriétaire original garde la valeur
 
let s = String::from("hello");
let len = calculate_length(&s);  // prête s à la fonction
println!("{s} a une longueur de {len}");  // ✅ s toujours valide

Références mutables (&mut T)

Une seule référence mutable peut exister à la fois. Elle permet de modifier la valeur.

fn append_world(s: &mut String) {
    s.push_str(", world");
}
 
let mut s = String::from("hello");
append_world(&mut s);
println!("{s}");  // "hello, world"

Règles du borrowing

SituationAutorisé
1 référence mutable OU N références immutables
1 référence mutable ET 1 référence immutable
2 références mutables simultanées
let mut s = String::from("hello");
 
let r1 = &s;      // ✅
let r2 = &s;      // ✅ (plusieurs immutables)
// let r3 = &mut s; // ⛔ (déjà emprunté immuablement)
 
println!("{r1} {r2}");  // les emprunts immuables se terminent ici
 
let r3 = &mut s;   // ✅ (les précédents ne sont plus utilisés)
r3.push_str("!");

Dangling references — le compilateur les empêche

fn dangle() -> &String {     // ⛔ retourne une référence vers...
    let s = String::from("hello");
    &s                        // ...une valeur qui sera libérée ici
}  // ← s libéré, la référence pointe vers rien
 
fn no_dangle() -> String {   // ✅ solution : retourner la valeur
    let s = String::from("hello");
    s                         // transfert d'ownership
}

1.3 Lifetimes — Garantir la validité des références

Les lifetimes sont des annotations qui indiquent au compilateur pendant combien de temps une référence est valide. Le compilateur les infère souvent tout seul.

Syntaxe

// 'a est un paramètre de lifetime : "les deux références doivent vivre au moins aussi longtemps que 'a"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Règles d’inférence (elision)

Dans la plupart des cas, le compilateur déduit les lifetimes tout seul :

fn first_word(s: &str) -> &str {  // lifetime implicite
    s.split_whitespace().next().unwrap_or("")
}
 
// Équivalent à :
fn first_word<'a>(s: &'a str) -> &'a str { ... }

Lifetime dans les structs

struct Texte<'a> {
    contenu: &'a str,  // le struct ne peut pas vivre plus longtemps que contenu
}
 
fn main() {
    let texte;
    {
        let data = String::from("hello");
        texte = Texte { contenu: &data };
        // ✅ data vit encore dans cette portée
    }
    // ⛔ data libérée, texte.contenu invalide
}

Static lifetime

'static signifie que la référence vit pendant toute la durée du programme :

let s: &'static str = "hello";  // string literal, toujours valide

Résumé des règles

ConceptRègle
Une valeurUn seul propriétaire à la fois
DropAutomatique à la sortie de portée
MoveTransfère la propriété, l’original devient invalide
Borrow &TN références immutables simultanées
Borrow &mut T1 seule référence mutable
LifetimeLa référence ne peut pas vivre plus longtemps que son emprunteur
DanglingEmpêché à la compilation — jamais de pointeur invalide

Exercices (rustlings)

// 1. Corriger : déplacer sans copier
fn main() {
    let s = String::from("hello");
    let t = s;  // s est moved
    println!("{s}");  // que faut-il changer ?
}
 
// 2. Corriger : emprunt mutable multiple
fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{r1} {r2}");
}
 
// 3. Lifetime : corriger
fn smallest<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if x < y { x } else { y }
}
 
fn main() {
    let x = 10;
    let result;
    {
        let y = 20;
        result = smallest(&x, &y);
    }
    println!("{result}");
}

🔗 Voir aussi