backend / rust
Borrowing et références
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.
| Situation | Autorisé ? | Raison |
|---|---|---|
Deux &T simultanées | Oui | Lecture seule, aucun risque de conflit |
Une &mut T seule | Oui | Écriture exclusive, personne d'autre ne lit en même temps |
Une &T et une &mut T en même temps | Non | Un lecteur pourrait voir une donnée en cours de modification |
Deux &mut T simultanées | Non | Deux é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.
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
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' :
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.