Aller au contenu

Erreur Rust E0502 : cannot borrow as mutable because it is also borrowed as immutable

E0502 veut dire que vous avez essayé de modifier une valeur alors qu’une autre partie de votre code était encore en train de la lire à travers une référence. Cette erreur apparaît sans arrêt avec les vecteurs, et la correction consiste généralement à déplacer une ligne ou à copier une valeur. Cette page vous donne d’abord la solution, puis explique pourquoi Rust y tient autant. Pour les autres erreurs de cette série, consultez les erreurs du compilateur Rust.

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; // a reference into the Vec
| - immutable borrow occurs here
4 | v.push(4); // ❌ changes the Vec while `first` is alive
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("First: {first}"); // ...because `first` is used here
| ----- immutable borrow later used here

Traduit, le compilateur dit : « impossible d’emprunter v en mutable, car il est aussi emprunté en immuable ». Les trois étiquettes indiquent où l’emprunt immuable commence, où l’emprunt mutable arrive, et où l’emprunt immuable sert encore ensuite.

Voici le programme qui la provoque :

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // a reference into the Vec
v.push(4); // ❌ changes the Vec while `first` is alive
println!("First: {first}"); // ...because `first` is used here
}

(Les commentaires disent : une référence dans le Vec ; on modifie le Vec pendant que first est vivante… parce que first est utilisée ici.)

Le compilateur pointe trois lignes, et il vous faut les trois pour comprendre l’erreur :

  1. Ligne 3 : &v[0] crée une référence en lecture seule vers v. C’est l’emprunt immuable.
  2. Ligne 4 : v.push(4) doit modifier v, donc il prend un emprunt mutable.
  3. Ligne 5 : first est utilisée à nouveau. C’est cette ligne qui garde le premier emprunt en vie pendant la ligne 4.

Sans la ligne 5, le code compilerait. Le problème n’est pas que les deux emprunts existent, c’est qu’ils se chevauchent.

La règle est : plusieurs lecteurs ou un seul rédacteur, jamais les deux en même temps. Vous l’avez vue à la page Le borrowing (l’emprunt).

Pour un Vec, il y a une raison très concrète. Un Vec garde ses éléments dans un seul bloc de mémoire. Quand vous faites push et que ce bloc est plein, le Vec déplace tout vers un nouveau bloc plus grand et libère l’ancien. Toute référence vers l’ancien bloc (comme first) pointe alors vers de la mémoire libérée. En C++, c’est le fameux bug d’« invalidation d’itérateur », qui peut faire planter votre programme ou lui faire lire en silence des données sans aucun sens.

C’est comme noter l’adresse d’un ami, puis il déménage dans une maison plus grande. Votre note a toujours l’air correcte, mais elle mène à un bâtiment vide. Rust refuse de compiler le code plutôt que de vous laisser visiter le bâtiment vide.

Commencez par celle-ci. Depuis Rust 2018, un emprunt se termine à sa dernière utilisation, pas à l’accolade fermante. C’est ce qu’on appelle les non-lexical lifetimes. Il suffit donc de finir d’utiliser la référence avant de modifier le vecteur :

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("Premier : {first}"); // dernière utilisation de `first` : l'emprunt se termine ici
v.push(4); // ✅ plus personne ne lit
println!("Maintenant : {:?}", v);
}

Sortie :

Premier : 1
Maintenant : [1, 2, 3, 4]
▶ Essayez par vous-même

Choisissez-la quand vous avez besoin de l’ancienne valeur après la modification. Prenez une copie au lieu d’une référence. Pour des nombres, enlevez simplement le &. Pour une String, utilisez .clone() :

fn main() {
let mut queue = vec![String::from("deploy"), String::from("test")];
let next = queue[0].clone(); // notre propre copie, pas une référence
queue.push(String::from("release"));
println!("Prochaine étape : {next}");
println!("File : {:?}", queue);
}

Sortie :

Prochaine étape : deploy
File : ["deploy", "test", "release"]
▶ Essayez par vous-même

Avec let first = v[0]; sur un Vec<i32>, vous obtenez une copie gratuitement, parce que i32 est Copy.

Solution 3 : utiliser iter_mut ou retain pour modifier sur place

Section intitulée « Solution 3 : utiliser iter_mut ou retain pour modifier sur place »

Choisissez-la quand vous avez écrit une boucle comme for p in prices.iter() puis essayé prices.remove(i) ou modifié des éléments à l’intérieur. Ça donne aussi E0502. La bibliothèque standard a des méthodes faites exactement pour ça :

fn main() {
let mut prices = vec![120, 45, 300, 80];
// modifier chaque élément sur place
for p in prices.iter_mut() {
*p = *p * 9 / 10; // 10 % de réduction
}
// retirer les éléments dont on ne veut pas
prices.retain(|&p| p >= 50);
println!("{:?}", prices);
}

Sortie :

[108, 270, 72]
▶ Essayez par vous-même

iter_mut vous donne un &mut par élément, il n’y a donc jamais qu’un seul rédacteur à la fois. retain fait le tri à votre place, sans risque, en un seul passage. Voir Les vecteurs et Les itérateurs.

Solution 4 : rassembler les changements, puis les appliquer

Section intitulée « Solution 4 : rassembler les changements, puis les appliquer »

Choisissez-la quand vous devez ajouter des éléments en fonction de ce que vous lisez. Lisez et rassemblez d’abord, puis écrivez dans une étape séparée :

fn main() {
let mut scores = vec![10, 25, 7, 40];
let bonuses: Vec<i32> = scores.iter().filter(|&&s| s > 20).map(|s| s * 2).collect();
scores.extend(bonuses); // l'emprunt en lecture est terminé, on peut écrire
println!("{:?}", scores);
}

Sortie :

[10, 25, 7, 40, 50, 80]
▶ Essayez par vous-même

Le Vec supplémentaire coûte un peu de mémoire, mais le code est clair et visiblement correct. C’est généralement un bon compromis.

Gardez-la en dernier recours, quand les autres solutions ne conviennent pas. Un indice n’est qu’un nombre, pas un emprunt, donc il ne bloque pas les modifications :

fn main() {
let mut scores = vec![10, 25, 7];
for i in 0..scores.len() {
if scores[i] > 20 {
scores.push(scores[i] * 2);
}
}
println!("{:?}", scores);
}

Sortie :

[10, 25, 7, 50]
▶ Essayez par vous-même

0..scores.len() est calculé une seule fois, avant le début de la boucle, donc les nouveaux éléments ne sont pas parcourus. Faites attention quand vous retirez des éléments par indice : les positions se décalent, et vous pouvez sauter des éléments ou provoquer un panic. Pour retirer, retain (solution 3) est plus sûr.

Ajouter à un Vec pendant qu’on le parcourt. for s in &scores { scores.push(..) } emprunte scores pendant toute la boucle. Correction : rassemblez d’abord et faites extend ensuite (solution 4), ou bouclez par indice (solution 5).

Appeler une méthode &mut self pendant qu’on parcourt un champ. C’est très courant dans les blocs impl :

fn audit(&mut self) {
for item in &self.items {
self.record(item); // ❌ cannot borrow `*self` as mutable because it is also borrowed as immutable
}
}

self.record emprunte tout self en mutable, y compris self.items. Correction : touchez directement l’autre champ (self.log.push(item.clone())), car Rust autorise l’emprunt de deux champs différents en même temps. Ou clonez d’abord les éléments. Voir Les structs.

Garder une valeur d’une HashMap pendant une insertion. let apples = stock.get("apples").unwrap();, puis stock.insert(..), puis une utilisation de apples donne E0502. Correction : copiez la valeur avec let apples = *stock.get("apples").unwrap(); ou .copied(). Voir HashMap.

  • E0499 : deux emprunts mutables en même temps. La même idée, avec deux rédacteurs au lieu d’un lecteur et d’un rédacteur.
  • E0382 : vous avez déplacé une valeur, puis vous l’avez utilisée. Apparaît souvent quand on corrige E0502 en déplaçant au lieu d’emprunter.
  • E0106 : un spécificateur de lifetime manquant sur une fonction qui renvoie une référence.
  • Toutes les erreurs du compilateur : la liste complète des pages d’erreurs.

Petit quiz

1. Pourquoi let first = &v[0]; v.push(4); println!("{first}"); ne compile-t-il pas ?

2. Vous voulez retirer tous les éléments inférieurs à 50 d'un Vec<i32>. Quelle est la correction la plus propre pour E0502 ?

3. Quand un emprunt immuable se termine-t-il en Rust moderne ?

Score : 0 / 3