Article

Corriger une erreur « cannot borrow as mutable » en Rust

L
22 septembre 2026 4 vues

Une erreur d’emprunt mutable en Rust ne se corrige pas toujours avec `mut`. Apprenez à suivre E0596, E0502 et E0499 jusqu’à la vraie cause.

Le compilateur Rust refuse parfois une ligne avec cannot borrow as mutable. Le message paraît viser l’opération qui modifie une valeur, mais la cause se trouve souvent quelques lignes plus haut : la valeur n’est pas mutable, la fonction n’a reçu qu’une référence partagée, ou un emprunt précédent est encore utilisé.

La bonne correction ne consiste donc pas à ajouter mut ou clone() jusqu’à ce que le code passe. Il faut suivre les annotations du diagnostic, repérer les accès qui se chevauchent, puis exprimer plus clairement l’ordre réel des opérations.

Lire le diagnostic comme une chronologie

Commencez par relancer une analyse courte. La première commande ci-dessous vérifie le projet sans produire l’exécutable final et permet d’itérer rapidement. Si le message comporte un code, la seconde donne l’explication longue fournie avec le compilateur.

cargo check
rustc --explain E0502

Ne vous arrêtez pas au titre de l’erreur. Les annotations reliées par des traits racontent généralement trois moments : l’endroit où le premier emprunt naît, l’opération où un nouvel accès incompatible est refusé, puis la dernière utilisation qui explique pourquoi le premier emprunt est toujours actif. Sur ce petit exemple, ces trois lignes sont visibles.

fn main() {
    let mut scores = vec![12, 18, 9];
    let leader = &scores[0];

    scores.push(21);
    println!("Score de référence : {leader}");
}
error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
  |
3 |     let leader = &scores[0];
  |                   ------ immutable borrow occurs here
5 |     scores.push(21);
  |     ^^^^^^^^^^^^^^^ mutable borrow occurs here
6 |     println!("Score de référence : {leader}");
  |                                     ------ immutable borrow later used here

La ligne 5 n’est pas mauvaise isolément. C’est son chevauchement avec l’emprunt créé ligne 3 et réutilisé ligne 6 qui pose problème. Cette lecture chronologique évite de modifier au hasard la ligne surlignée en rouge.

Ce que &mut garantit vraiment

&T donne un accès partagé en lecture. &mut T donne un accès mutable qui doit rester exclusif pendant la durée où cette référence peut encore servir. Le compilateur ne sanctionne donc pas une simple faute de syntaxe : il empêche qu’une mutation invalide une autre référence ou rende deux accès concurrents incohérents.

Avec un Vec, l’enjeu est concret. Un push peut réallouer le stockage du vecteur. Une référence vers l’un de ses éléments ne peut pas rester utilisable pendant cette opération, car elle pourrait alors pointer vers l’ancien emplacement.

De la liaison à la signature

Le cas le plus direct correspond souvent à E0596. La collection est possédée localement, mais sa liaison n’autorise pas la mutation.

La valeur locale doit vraiment changer

Ce code échoue parce que push a besoin d’emprunter notes de façon mutable.

fn main() {
    let notes = vec!["ownership"];
    notes.push("borrowing");
}

Si l’intention est bien de faire évoluer cette variable, la correction se trouve sur la liaison qui possède le vecteur.

fn main() {
    let mut notes = vec!["ownership"];
    notes.push("borrowing");

    assert_eq!(notes, ["ownership", "borrowing"]);
}

Le contrat de la fonction doit annoncer la mutation

Ajouter mut chez l’appelant ne suffit pas si la fonction reçoit &Vec<String>. Cette signature promet un accès partagé, alors que son corps tente de modifier la collection.

fn add_tag(tags: &Vec<String>) {
    tags.push(String::from("rust"));
}

Dans ce cas, le changement concerne le contrat complet : la fonction accepte &mut Vec<String> et l’appelant lui prête une collection mutable. Le type rend alors l’effet de bord visible aux deux endroits.

fn add_tag(tags: &mut Vec<String>) {
    tags.push(String::from("rust"));
}

fn main() {
    let mut tags = Vec::new();
    add_tag(&mut tags);

    assert_eq!(tags, ["rust"]);
}

Si la fonction n’est pas censée modifier la valeur, conservez plutôt &T et retirez la mutation de son corps. Le compilateur force ici une décision d’API, pas seulement une correction locale.

Quand une lecture reste active

E0502 apparaît lorsqu’un emprunt d’une certaine mutabilité rencontre un nouvel emprunt incompatible. Dans l’exemple des scores, la référence leader est encore utilisée après push. Déplacer sa dernière lecture avant la mutation suffit à rendre les deux périodes disjointes.

fn main() {
    let mut scores = vec![12, 18, 9];
    let leader = &scores[0];

    println!("Score de référence : {leader}");
    scores.push(21);

    assert_eq!(scores, [12, 18, 9, 21]);
}

Les durées de vie non lexicales permettent au compilateur de terminer l’emprunt après sa dernière utilisation réelle, sans attendre l’accolade de fin de bloc. Ici, leader existe toujours comme nom, mais il n’est plus utilisé après le println! : l’emprunt peut donc prendre fin avant push.

Si l’ordre ne peut pas changer, demandez-vous ce dont la suite a réellement besoin. Pour un entier Copy, conserver la valeur plutôt qu’une référence exprime souvent mieux l’intention. Pour un calcul, effectuez-le avant d’ouvrir l’emprunt mutable. Un bloc plus court peut aussi matérialiser une phase de lecture, puis laisser commencer la phase de modification.

fn main() {
    let mut scores = vec![12, 18, 9];

    let average = {
        let total: i32 = scores.iter().sum();
        total / scores.len() as i32
    };

    scores.push(average);
    assert_eq!(scores, [12, 18, 9, 13]);
}

Séparer deux accès mutables

E0499 signale deux emprunts mutables qui se chevauchent. Même si les indices sont différents à l’œil nu, deux indexations successives repartent toutes les deux du même vecteur, et la première référence reste utilisée après la création de la seconde.

fn main() {
    let mut scores = vec![12, 18, 9];
    let first = &mut scores[0];
    let second = &mut scores[1];

    *first += 1;
    *second += 1;
}

Lorsque les zones sont réellement distinctes, utilisez une API qui encode cette séparation. split_at_mut produit deux tranches mutables non superposées ; le type retourné apporte au compilateur la preuve qui manquait.

fn main() {
    let mut scores = vec![12, 18, 9];
    let (first_part, second_part) = scores.split_at_mut(1);
    let first = &mut first_part[0];
    let second = &mut second_part[0];

    *first += 1;
    *second += 1;

    assert_eq!(scores, [13, 19, 9]);
}

Pour quelques indices non contigus, la bibliothèque standard propose aussi get_disjoint_mut, qui vérifie que les indices sont valides et distincts. S’il n’est pas nécessaire de conserver deux références en même temps, une solution encore plus simple consiste à effectuer les mutations l’une après l’autre.

Restructurer sans cloner par réflexe

Un clone() peut faire disparaître un emprunt parce qu’il crée une nouvelle valeur possédée. Cela peut être exactement ce que demande le métier : un instantané indépendant, une donnée conservée après la modification de sa source, ou un objet volontairement transmis à une autre tâche. Mais cloner seulement pour calmer le compilateur ajoute parfois une allocation, masque l’ordre des opérations et laisse le vrai contrat du code inchangé.

Avec une table de hachage, évitez par exemple de garder une référence obtenue par get() pendant un insert() sur la même collection. Si la valeur est petite et Copy, extrayez-la d’abord ; l’emprunt partagé se termine avant la mutation.

use std::collections::HashMap;

fn main() {
    let mut visits = HashMap::from([(String::from("rust"), 4_u32)]);
    let next = visits.get("rust").copied().unwrap_or(0) + 1;
    visits.insert(String::from("rust"), next);

    assert_eq!(visits["rust"], 5);
}

Si le conflit revient partout, il peut révéler une structure trop large : une méthode emprunte toute une structure alors qu’elle ne modifie qu’un champ, ou deux étapes métier sont entremêlées. Séparer les champs, calculer les entrées avant la mutation ou choisir une méthode de collection adaptée donne généralement un code plus lisible que l’ajout de copies.

RefCell, Mutex et RwLock répondent à d’autres contraintes d’architecture. RefCell déplace la vérification des emprunts à l’exécution dans un contexte monothread ; Mutex et RwLock organisent des accès partagés synchronisés. Ils ne sont pas des rustines universelles, pas plus que unsafe n’est une manière acceptable de contourner un conflit d’emprunt mal compris.

Retrouver l’intention du code

Face à cannot borrow as mutable, suivez le diagnostic dans le temps : où le premier emprunt commence-t-il, où le nouvel accès est-il refusé, et quelle utilisation maintient encore l’ancien emprunt en vie ? Si la valeur doit changer, rendez la liaison et la signature honnêtes. Si deux accès se chevauchent, réordonnez les opérations ou utilisez une API qui prouve leur séparation.

Le borrow checker devient alors moins mystérieux : il met en évidence une ambiguïté de structure. Une correction réussie ne fait pas seulement compiler le programme ; elle rend également visible qui lit, qui modifie et pendant combien de temps.

Questions fréquentes

01

Quelle différence entre mut x et &mut x ?

let mut x autorise la liaison propriétaire à changer. &mut x crée une référence mutable exclusive vers cette valeur pour une durée limitée. Une API qui modifie une valeur empruntée a généralement besoin des deux côtés : un propriétaire mutable et un paramètre &mut T.

02

Pourquoi l’erreur semble-t-elle pointer la mauvaise ligne ?

La ligne refusée peut être correcte prise seule. Elle devient incompatible avec un emprunt créé auparavant et encore utilisé ensuite. Lisez toutes les annotations du diagnostic avant de modifier le code.

03

Faut-il ajouter un bloc pour terminer chaque emprunt ?

Non. Grâce aux durées de vie non lexicales, un emprunt se termine souvent après sa dernière utilisation réelle. Un bloc explicite reste utile quand il clarifie deux phases du traitement ou raccourcit volontairement la portée de plusieurs références.

04

Quand un clone() est-il raisonnable ?

Lorsqu’une copie indépendante correspond au besoin fonctionnel et que son coût est accepté. S’il sert uniquement à contourner un chevauchement temporaire, commencez par réordonner les opérations ou réduire la durée de l’emprunt.

Partager cet article

Partager
L

Écrit par

larevuegeek

À lire aussi

Commentaires (0)

Connectez-vous pour laisser un commentaire.