Aller au contenu

Erreur Rust E0499 : cannot borrow as mutable more than once at a time

L’erreur E0499 veut dire que vous avez essayé d’avoir deux références mutables (&mut) vers la même valeur en même temps. C’est l’une des erreurs les plus courantes du borrow checker (le vérificateur d’emprunts), et la correction est généralement petite : souvent, il suffit de déplacer une ligne. (Vous cherchez une autre erreur ? Consultez la vue d’ensemble des erreurs du compilateur.)

error[E0499]: cannot borrow `balance` as mutable more than once at a time
--> src/main.rs:5:20
|
4 | let deposit = &mut balance;
| ------------ first mutable borrow occurs here
5 | let withdraw = &mut balance;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | *deposit += 50;
| -------------- first borrow later used here

Traduit, le compilateur dit : « impossible d’emprunter balance en mutable plus d’une fois à la fois ». Il montre où le premier emprunt mutable commence, où le second arrive, et où le premier sert encore ensuite.

Voici le programme qui la provoque :

fn main() {
let mut balance = 100;
let deposit = &mut balance;
let withdraw = &mut balance;
*deposit += 50;
*withdraw -= 30;
println!("Balance: {balance}");
}

Le compilateur pointe trois endroits, et ensemble ils racontent toute l’histoire :

  1. Ligne 4 : deposit devient un emprunt mutable de balance.
  2. Ligne 5 : withdraw essaie de devenir un deuxième emprunt mutable de balance.
  3. Ligne 7 : deposit est encore utilisé après la ligne 5. Le premier emprunt est donc toujours vivant quand le second commence.

C’est cette troisième étiquette qui compte. Deux emprunts &mut ne posent problème que si leurs durées de vie se chevauchent. Si deposit avait fini son travail avant la ligne 5, il n’y aurait pas d’erreur.

Imaginez un document partagé avec une seule clé « modification ». Celui qui tient la clé peut modifier le document. Rust ne fera pas de double de la clé : la deuxième personne doit attendre que la première la rende.

La règle du borrowing (l’emprunt) en Rust est : à tout moment, vous pouvez avoir plusieurs lecteurs (&) ou un seul rédacteur (&mut), jamais plus d’un rédacteur. Une référence &mut est une promesse : « tant que j’existe, personne d’autre ne touche à cette valeur ».

Cette promesse évite de vrais bugs. Si deux morceaux de code pouvaient modifier la même valeur en même temps, l’un pourrait redimensionner un Vec pendant que l’autre pointe encore vers son ancienne mémoire. Dans d’autres langages, ça donne un plantage ou une corruption silencieuse des données. Entre plusieurs threads, c’est une « data race » (une course aux données).

Grâce à cette règle, le compilateur sait qu’un &mut est le seul moyen d’atteindre les données tant qu’il existe. Vous obtenez cette sécurité sans aucun coût à l’exécution. La contrepartie, c’est que vous devez parfois montrer plus clairement au compilateur que vos deux emprunts ne se gênent pas vraiment.

Solution 1 : terminer le premier emprunt avant de commencer le second

Section intitulée « Solution 1 : terminer le premier emprunt avant de commencer le second »

Commencez par celle-ci. Elle corrige la plupart des erreurs E0499. Un emprunt dure jusqu’à sa dernière utilisation, pas jusqu’à la fin du bloc. Faites donc tout le travail avec la première référence, puis créez la seconde :

fn main() {
let mut balance = 100;
let deposit = &mut balance;
*deposit += 50; // dernière utilisation de `deposit` : son emprunt se termine ici
let withdraw = &mut balance; // ✅ plus personne n'emprunte maintenant
*withdraw -= 30;
println!("Solde : {balance}");
}

Sortie :

Solde : 120
▶ Essayez par vous-même

Souvent, vous n’avez même pas besoin des variables supplémentaires : balance += 50; puis balance -= 30; fonctionne tout aussi bien.

Solution 2 : emprunter des parties qui ne se chevauchent pas

Section intitulée « Solution 2 : emprunter des parties qui ne se chevauchent pas »

Parfois, vous avez vraiment besoin de deux &mut en même temps, mais ils pointent vers des parties différentes des données. Le compilateur ne peut pas toujours le voir, alors vous le lui montrez.

Pour deux éléments d’un Vec ou d’une slice, tasks[0] et tasks[1] empruntent tous les deux tasks en entier. Utilisez split_at_mut (en anglais) pour le couper en deux moitiés dont on est sûr qu’elles ne se chevauchent pas :

fn main() {
let mut tasks = vec![String::from("write"), String::from("test")];
// Couper le Vec en deux moitiés qui ne se chevauchent pas
let (left, right) = tasks.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
std::mem::swap(first, second);
println!("{tasks:?}");
}

Sortie :

["test", "write"]
▶ Essayez par vous-même

(Pour un simple échange, tasks.swap(0, 1) est encore plus simple. split_at_mut sert quand vous avez besoin des deux références pour un vrai travail.)

Pour une struct, la même erreur apparaît quand vous appelez deux méthodes qui prennent chacune &mut self, comme p.name_mut() et p.score_mut(). Chaque appel emprunte la struct entière. Si vous empruntez directement les champs, le compilateur voit qu’ils sont séparés :

struct Player {
name: String,
score: u32,
}
fn main() {
let mut p = Player { name: String::from("Ferris"), score: 0 };
// Emprunter les champs directement, pas via des méthodes
let name = &mut p.name;
let score = &mut p.score;
name.push_str(" le crabe");
*score += 10;
println!("{} a {} points", p.name, p.score);
}

Sortie :

Ferris le crabe a 10 points
▶ Essayez par vous-même

Solution 3 : utiliser des indices au lieu de garder des références

Section intitulée « Solution 3 : utiliser des indices au lieu de garder des références »

Un grand classique : vous parcourez un Vec avec iter_mut() et vous essayez aussi d’y faire un push dans la boucle. L’itérateur garde un &mut pendant toute la boucle, donc push en est un deuxième :

fn main() {
let mut queue = vec![1, 2, 3];
for job in queue.iter_mut() {
*job *= 10;
queue.push(*job); // ❌ E0499: `queue` is already borrowed by iter_mut()
}
}

(Le commentaire dit : queue est déjà emprunté par iter_mut().) Quand la boucle doit modifier la collection elle-même, parcourez plutôt les positions. Un indice n’est qu’un nombre, il n’emprunte donc rien :

fn main() {
let mut queue = vec![1, 2, 3];
// Parcourir les positions au lieu de garder une référence dans `queue`
for i in 0..queue.len() {
queue[i] *= 10;
let job = queue[i];
queue.push(job);
}
println!("{queue:?}");
}

Sortie :

[10, 20, 30, 10, 20, 30]
▶ Essayez par vous-même

0..queue.len() est calculé une seule fois, avant le début de la boucle, donc les nouveaux éléments ne sont pas parcourus à nouveau. Chaque emprunt queue[i] ne dure qu’une ligne.

RefCell déplace la vérification des emprunts de la compilation vers l’exécution. Vous pouvez appeler borrow_mut() sans variable mut, tant que deux emprunts mutables ne sont jamais vivants au même moment :

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100); // remarque : pas besoin de `mut`
*balance.borrow_mut() += 50; // on emprunte, on modifie, l'emprunt se termine au `;`
*balance.borrow_mut() -= 30;
println!("Solde : {}", balance.borrow());
}

Sortie :

Solde : 120
▶ Essayez par vous-même

Soyons honnêtes : RefCell ne supprime pas la règle, il la vérifie seulement plus tard. Si vous gardez deux gardes borrow_mut() vivants en même temps, le programme compile mais fait un panic à l’exécution :

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100);
let mut deposit = balance.borrow_mut();
let mut withdraw = balance.borrow_mut(); // 💥 `deposit` est encore vivant
*deposit += 50;
*withdraw -= 30;
}
thread 'main' (12345) panicked at src/main.rs:7:32:
RefCell already borrowed

Le message dit que la RefCell est déjà empruntée. Ne sortez donc RefCell que lorsque les données sont vraiment partagées (par exemple dans un Rc<RefCell<T>>) et que les solutions 1 à 3 ne conviennent pas. Ce n’est pas un moyen de faire taire le compilateur.

  • Stocker des &mut dans une boucle. handles.push(&mut counter) dans une boucle for échoue avec « mutably borrowed here in the previous iteration of the loop » (emprunté en mutable ici lors du tour précédent de la boucle). Chaque tour ajoute un nouveau &mut vers la même valeur. Correction : stockez des indices ou des valeurs possédées, ou modifiez la valeur directement dans la boucle.
  • Deux appels à get_mut sur une HashMap. let a = stock.get_mut("apples") puis let p = stock.get_mut("pears") empruntent stock deux fois. Correction : finissez avec a avant de rappeler get_mut, ou utilisez stock.get_disjoint_mut(["apples", "pears"]) pour obtenir les deux d’un coup.
  • Deux closures qui modifient la même variable. let mut inc = || count += 1; et let mut inc2 = || count += 2; gardent chacune un &mut count tant qu’elles existent. Correction : utilisez une seule closure, ou appelez la première pour la dernière fois avant de créer la seconde. Voir Les closures.

Petit quiz

1. Quand Rust signale-t-il E0499 ?

2. Vous créez let a = &mut v;, utilisez a pour la dernière fois, puis créez let b = &mut v;. Que se passe-t-il ?

3. Quel outil permet d'obtenir deux références &mut vers des éléments différents d'une même slice ?

Score : 0 / 3