Aller au contenu

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.

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 : 5
Toujours à moi : hello
▶ Essayez par vous-même

Le 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 !!!
▶ Essayez par vous-même

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.

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 immutable

Autrement dit : impossible d’emprunter text de façon mutable, car il est déjà emprunté en lecture seule.

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 : bonjour
bonjour, le monde
▶ Essayez par vous-même

La 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.

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.

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.

É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 à main
▶ Essayez par vous-même

La 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.)

É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
- pommes
▶ Essayez par vous-même

add_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.

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 here
4 | let second = &mut message;
| ^^^^^^^^^^^^ second mutable borrow occurs here
5 | first.push_str(" 10");
| ----- first borrow later used here

Le 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 points
▶ Essayez par vous-même

Dans 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.

  • 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 &mut pour 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

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