Retour au cours

backend / rust

Borrowing et références

Leçon 51 exercice

Explication

Ce que vous allez apprendre

  • Emprunter une valeur avec & sans en prendre l'ownership
  • Créer une référence mutable exclusive avec &mut
  • Comprendre la règle d'or du borrow checker : N lectures OU une seule écriture
  • Comprendre pourquoi une dangling reference est structurellement impossible en Rust
  • Utiliser les slices (&str, &[T]) pour référencer une portion d'une collection

Dans quel contexte ?

Après avoir découvert dans la leçon précédente que passer une String à une fonction en transfère l'ownership, un développeur se demande comment calculer la longueur d'une chaîne sans la "perdre" pour le reste du programme. La réponse s'appelle le borrowing : prêter temporairement l'accès à une donnée, sans jamais en céder la propriété.

D'abord, emprunter résout exactement le problème de la leçon précédente

fn calculer_longueur(s: &String) -> usize prend une référence (&String) plutôt que la valeur elle-même. La fonction peut lire la donnée, mais n'en devient jamais propriétaire : à la fin de la fonction, rien n'est libéré, car s ne possédait rien. La variable d'origine, à l'appel, reste parfaitement utilisable après.

Une référence mutable (&mut String) va plus loin : elle permet à la fonction de modifier réellement la donnée d'origine, sans pour autant en transférer la propriété.

Une fois ce mécanisme acquis, il faut retenir la règle la plus importante de Rust

Le "borrow checker" impose une règle stricte et non négociable : à un instant donné, on peut avoir soit plusieurs références immuables simultanées vers une même donnée, soit une seule référence mutable exclusive — jamais les deux en même temps.

SituationAutorisé ?Raison
Deux &T simultanéesOuiLecture seule, aucun risque de conflit
Une &mut T seuleOuiÉcriture exclusive, personne d'autre ne lit en même temps
Une &T et une &mut T en même tempsNonUn lecteur pourrait voir une donnée en cours de modification
Deux &mut T simultanéesNonDeux écritures concurrentes = incohérence garantie

Prérequis

Il est indispensable d'avoir bien compris l'ownership (leçon précédente) avant d'aborder le borrowing : ce sont les deux faces d'un même mécanisme de sécurité mémoire.

Il reste une subtilité moderne à connaître : les lifetimes non-lexicaux

Le compilateur Rust moderne (grâce au "NLL", non-lexical lifetimes) est assez intelligent pour comprendre qu'une référence n'est "active" que jusqu'à sa dernière utilisation réelle, pas jusqu'à la fin syntaxique du bloc qui la contient. Cela permet d'obtenir une référence mutable juste après avoir fini d'utiliser des références immuables, dans la même fonction, sans erreur de compilation.

Piège fréquent

Tenter de retourner une référence vers une variable créée localement dans une fonction (fn f() -> &String { let s = String::from("x"); &s }) ne compile jamais : s serait détruite à la fin de la fonction, et la référence retournée pointerait vers rien. C'est ce qu'on appelle une "dangling reference", et Rust la rend structurellement impossible — contrairement à C ou C++ où ce bug provoque un comportement indéfini à l'exécution.

Enfin, un slice emprunte une portion d'une collection sans la copier

&phrase[0..2] référence uniquement les deux premiers octets de phrase, sans dupliquer la moindre donnée. &str (une slice de chaîne) et &[T] (une slice de tableau) sont d'ailleurs des types de référence à part entière, omniprésents dans le code Rust idiomatique.

Bonne pratique

Préfère systématiquement &str à &String en paramètre de fonction quand tu n'as besoin que de lire une chaîne : cela accepte à la fois des String et des littéraux de chaîne, rendant ta fonction plus flexible pour l'appelant.

Maintenant que tu sais emprunter une valeur en toute sécurité, la prochaine leçon formalise un concept déjà esquissé ici : les lifetimes, qui garantissent qu'aucune référence ne survit jamais à sa donnée.

Commandes & code

Borrowing et références

Emprunter une valeur permet de l'utiliser sans en prendre l'ownership.

rust
fn calculer_longueur(s: &String) -> usize { // &String : emprunt immuable
    s.len()
} // s sort du scope mais NE POSSEDE PAS la donnee, rien n'est libere

fn ajouter_suffixe(s: &mut String) { // &mut String : emprunt mutable
    s.push_str(" (modifie)");
}

fn main() {
    let s1 = String::from("hello");
    let longueur = calculer_longueur(&s1); // on prete une reference, pas l'ownership
    println!("{} a une longueur de {}", s1, longueur); // s1 toujours valide !

    let mut s2 = String::from("hello");
    ajouter_suffixe(&mut s2);
    println!("{}", s2);

    // REGLE D'OR du borrow checker :
    // - soit plusieurs references IMMUABLES simultanees
    // - soit UNE SEULE reference MUTABLE, jamais les deux en meme temps
    let mut s3 = String::from("valeur");

    let r1 = &s3;
    let r2 = &s3; // OK : plusieurs emprunts immuables simultanes
    println!("{} {}", r1, r2);
    // r1 et r2 ne sont plus utilises apres cette ligne (NLL: non-lexical lifetimes)

    let r3 = &mut s3; // OK maintenant : r1/r2 ne sont plus utilises
    r3.push_str(" modifiee");
    println!("{}", r3);

    // ERREUR classique : emprunt mutable + immuable en meme temps
    // let r4 = &s3;
    // let r5 = &mut s3; // ERREUR: cannot borrow as mutable while borrowed as immutable

    // Dangling reference : IMPOSSIBLE en Rust, empeche a la compilation
    // fn reference_pendante() -> &String {
    //     let s = String::from("oups");
    //     &s // ERREUR: s serait droppe, la reference serait invalide
    // }

    // Slices : reference vers une portion d'une collection, sans copie
    let phrase = String::from("le rust est rapide");
    let premier_mot = &phrase[0..2];  // "le"
    let reste = &phrase[3..];          // "rust est rapide"
    println!("{} | {}", premier_mot, reste);

    let tableau = [1, 2, 3, 4, 5];
    let sous_tableau: &[i32] = &tableau[1..3]; // [2, 3]
    println!("{:?}", sous_tableau);
}

Résumé

  • Emprunter (&T, &mut T) donne un accès temporaire sans transférer l'ownership.
  • Règle du borrow checker : N références immuables ou une seule référence mutable, jamais mélangées.
  • Les dangling references sont impossibles : le compilateur refuse toute référence qui survivrait à sa donnée.
  • Les slices (&str, &[T]) empruntent une portion contiguë d'une collection sans la copier.

Exercices pratiques

1 disponible
1

Mission : debloquer un cache qui refuse de compiler

Objectif : Diagnostiquer une violation de la regle du borrow checker et choisir la reorganisation de code qui la resout sans clone inutile.

Contexte

Un collegue a ecrit un petit cache en memoire et obtient l'erreur 'cannot borrow cache as mutable because it is also borrowed as immutable' :

rust
fn main() {
    let mut cache: Vec<String> = vec![String::from("a"), String::from("b")];
    let premier = &cache[0];
    cache.push(String::from("c"));
    println!("{}", premier);
}

Il ne comprend pas pourquoi push declenche une erreur alors qu'il ne touche pas directement a premier. Aide-le a comprendre la regle exacte qui est violee, et corrige le code.

Résoudre l’exercice →