Le borrowing en Rust
Sur la page consacrée à l’ownership, vous avez rencontré un problème : passer une String à une fonction la déplace, et vous ne pouvez plus l’utiliser ensuite. Déplacer (ou cloner) les valeurs sans arrêt deviendrait vite pénible. La solution de Rust, c’est le borrowing (emprunt).
Emprunter, c’est laisser un morceau de code utiliser une valeur sans en prendre l’ownership, comme quand vous prêtez un livre à un ami. Il le lit, puis vous le rend, et le livre est toujours à vous.
Les références : emprunter avec &
Section intitulée « Les références : emprunter avec & »Vous créez une référence en plaçant & devant une valeur. Une référence permet à une fonction de consulter les données sans les posséder :
fn print_length(s: &String) { // emprunte s, sans le posséder println!("Longueur : {}", s.len());}
fn main() { let text = String::from("hello"); print_length(&text); // on prête text à la fonction println!("Toujours à moi : {text}"); // ✅ text est encore valide ici}Sortie :
Longueur : 5Toujours à moi : helloLe texte reste ici en anglais (hello) parce que len() compte des octets, et qu’une lettre accentuée en occupe deux : le résultat ne correspondrait plus au nombre de lettres que vous voyez.
Le &text signifie « laisse cette fonction emprunter text ». Comme la fonction l’a seulement emprunté, l’ownership reste dans main, et vous pouvez continuer à utiliser text après l’appel. Comparez avec la page sur l’ownership, où le même code sans & provoquait une erreur.
Les références mutables : emprunter pour modifier
Section intitulée « Les références mutables : emprunter pour modifier »Une simple référence & est en lecture seule : vous pouvez regarder, mais pas toucher. Si une fonction doit modifier la valeur empruntée, utilisez une référence mutable, avec &mut :
fn add_excitement(s: &mut String) { s.push_str(" !!!");}
fn main() { let mut text = String::from("bonjour"); add_excitement(&mut text); // on la prête de façon mutable println!("{text}"); // bonjour !!!}Sortie :
bonjour !!!Remarquez que trois choses doivent concorder : text est déclaré mut, la fonction demande un &mut String, et l’appel passe &mut text. Les trois sont nécessaires pour modifier une valeur empruntée.
Les règles d’emprunt
Section intitulée « Les règles d’emprunt »Rust impose deux règles sur les références. Elles servent à empêcher les data races (accès concurrents aux données) : toute une famille de bugs où deux morceaux de code touchent les mêmes données en même temps et finissent par les corrompre.
À tout moment, vous pouvez avoir soit :
- autant de références en lecture seule (
&) que vous voulez, soit - une seule référence mutable (
&mut),
mais jamais les deux à la fois.
Pour le dire simplement : plusieurs lecteurs ou un seul rédacteur, jamais les deux ensemble. Ce code enfreint la règle :
fn main() { let mut text = String::from("bonjour");
let r1 = &text; // un emprunt en lecture seule let r2 = &mut text; // ❌ impossible d'emprunter en mutable tant que r1 existe
println!("{r1} {r2}");}Le compilateur vous arrête :
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutableAutrement dit : impossible d’emprunter text de façon mutable, car il est déjà emprunté en lecture seule.
Quand un emprunt se termine-t-il ?
Section intitulée « Quand un emprunt se termine-t-il ? »Vous pourriez penser qu’un emprunt dure jusqu’à l’accolade fermante } de sa portée. Ce n’est pas le cas. Un emprunt dure seulement jusqu’au dernier endroit où la référence est utilisée. Ensuite, la valeur peut de nouveau être empruntée, même de façon mutable.
Cela veut dire que l’exemple cassé ci-dessus se corrige simplement en finissant d’utiliser r1 avant de créer r2 :
fn main() { let mut text = String::from("bonjour");
let r1 = &text; println!("Lecture : {r1}"); // dernière utilisation de r1 : son emprunt se termine ici
let r2 = &mut text; // ✅ pas de problème : plus personne ne lit r2.push_str(", le monde");
println!("{text}");}Sortie :
Lecture : bonjourbonjour, le mondeLa règle reste « plusieurs lecteurs ou un seul rédacteur ». Simplement, elle est vérifiée sur la portion de code où chaque référence est réellement utilisée. Le lecteur r1 a fini quand le rédacteur r2 apparaît, et r2 a fini avant que le dernier println! relise text. Rien ne se chevauche, donc Rust est content.
C’est la façon la plus courante de corriger une erreur d’emprunt : changer l’ordre. Faites toutes les lectures dont vous avez besoin, puis les écritures (ou l’inverse), au lieu de les mélanger.
Pourquoi c’est important
Section intitulée « Pourquoi c’est important »Ces règles sont vérifiées entièrement à la compilation : elles ne coûtent donc rien pendant l’exécution du programme. C’est ainsi que Rust peut promettre « aucune data race » tout en étant aussi rapide que le C : la sécurité est prouvée avant même que le programme démarre.
Exercices
Section intitulée « Exercices »Essayez d’abord chaque exercice vous-même, dans le Rust Playground ou dans un projet local. Si le compilateur se plaint, lisez l’erreur : elle montre généralement exactement où les emprunts se chevauchent.
1. Compter sans prendre
Section intitulée « 1. Compter sans prendre »Écrivez une fonction total_letters qui emprunte un Vec<String> et renvoie le nombre total de lettres de tous les mots. Après l’appel, main doit toujours pouvoir afficher le vecteur.
Indice : le paramètre est de type &Vec<String>, et vous l’appelez avec &words. Parcourez le vecteur emprunté avec for word in list et additionnez les word.len().
Voir la solution
fn total_letters(list: &Vec<String>) -> usize { let mut total = 0; for word in list { total += word.len(); } total}
fn main() { let words = vec![String::from("borrow"), String::from("check")]; let count = total_letters(&words); // on prête le vecteur println!("{count} lettres"); println!("{:?} appartient toujours à main", words);}Sortie :
11 lettres["borrow", "check"] appartient toujours à mainLa fonction a seulement besoin de regarder les mots, donc un emprunt en lecture seule avec & suffit. Si vous écriviez list: Vec<String> à la place, le vecteur serait déplacé dans la fonction, et le dernier println! ne compilerait pas. (Les mots restent en anglais pour la même raison que plus haut : len() compte des octets.)
2. Une liste de courses
Section intitulée « 2. Une liste de courses »Écrivez deux fonctions : add_item, qui ajoute un article à une liste de courses, et print_list, qui affiche tous les articles. Ajoutez trois articles, puis affichez la liste.
Indice : une fonction modifie la liste, l’autre se contente de la lire. Laquelle a besoin de &mut Vec<String>, et laquelle peut se contenter de &Vec<String> ? Pour transformer un &str en String, utilisez String::from(item).
Voir la solution
fn add_item(list: &mut Vec<String>, item: &str) { list.push(String::from(item));}
fn print_list(list: &Vec<String>) { for item in list { println!("- {item}"); }}
fn main() { let mut list = Vec::new(); add_item(&mut list, "lait"); add_item(&mut list, "pain"); add_item(&mut list, "pommes"); print_list(&list);}Sortie :
- lait- pain- pommesadd_item modifie la liste : elle reçoit donc un emprunt &mut, et list doit être déclarée mut dans main. print_list ne fait que lire, donc un simple & suffit. Chaque emprunt &mut list se termine quand l’appel revient, et c’est pour cela que l’appel suivant peut l’emprunter à nouveau.
3. Deux rédacteurs à la fois
Section intitulée « 3. Deux rédacteurs à la fois »Ce programme ne compile pas. Corrigez-le pour qu’il affiche Score : 10 points, en gardant à la fois first et second.
fn main() { let mut message = String::from("Score :"); let first = &mut message; let second = &mut message; first.push_str(" 10"); second.push_str(" points"); println!("{message}");}error[E0499]: cannot borrow `message` as mutable more than once at a time --> src/main.rs:4:18 |3 | let first = &mut message; | ------------ first mutable borrow occurs here4 | let second = &mut message; | ^^^^^^^^^^^^ second mutable borrow occurs here5 | first.push_str(" 10"); | ----- first borrow later used hereLe compilateur explique qu’un premier emprunt mutable a lieu à la ligne 3, un deuxième à la ligne 4, alors que le premier sert encore à la ligne 5.
Indice : un seul rédacteur à la fois. Relisez Quand un emprunt se termine-t-il ? plus haut : pouvez-vous finir d’utiliser first avant de créer second ?
Voir la solution
fn main() { let mut message = String::from("Score :");
let first = &mut message; first.push_str(" 10"); // dernière utilisation de first : son emprunt se termine ici
let second = &mut message; second.push_str(" points");
println!("{message}");}Sortie :
Score : 10 pointsDans la version d’origine, first devait encore servir (à la ligne 5) au moment où second était créé, à la ligne 4. Cela faisait deux rédacteurs en même temps. En remontant first.push_str, on a fini avec first avant que second existe : il n’y a donc jamais plus d’un &mut à la fois.
Récapitulatif
Section intitulée « Récapitulatif »- Le borrowing (
&) permet à du code d’utiliser une valeur sans en prendre l’ownership : on vous la rend ensuite. - Une simple référence
&est en lecture seule ; utilisez&mutpour modifier la valeur empruntée. - Pour modifier une valeur à travers une référence, la variable, le paramètre et l’appel ont tous besoin de
mut/&mut. - La règle : plusieurs lecteurs ou un seul rédacteur, jamais les deux à la fois.
- Un emprunt se termine à la dernière utilisation de la référence : réorganiser votre code suffit donc souvent à corriger une erreur d’emprunt.
- Tout cela est vérifié à la compilation, donc gratuit à l’exécution.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que fait un & placé devant une valeur quand on la passe à une fonction ?
2. D'après les règles d'emprunt, quelle combinaison est autorisée en même temps ?
3. De quoi avez-vous besoin pour modifier une valeur à travers une référence ?
4. Pourquoi Rust peut-il imposer les règles d'emprunt sans aucun coût en performances à l'exécution ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- References and Borrowing — The Rust Book (en anglais) : le chapitre officiel, y compris les références pendantes (dangling references) et pourquoi Rust refuse d’en créer.
- Borrowing — Rust by Example (en anglais) : des exemples exécutables d’emprunts avec
&, suivis de pages sur les emprunts mutables et l’aliasing. - Non-lexical lifetimes — The Edition Guide (en anglais) : le changement qui fait qu’un emprunt se termine à sa dernière utilisation, et non plus à l’accolade fermante.
- The Slice Type — The Rust Book (en anglais) : emprunter seulement une partie d’une chaîne ou d’un tableau, l’étape suivante logique après les références.
reference— documentation de la bibliothèque standard (en anglais) : ce que sont&Tet&mut T, et quels traits ils implémentent.