Aller au contenu

Erreur Rust E0594 : cannot assign, as it is not declared as mutable

E0594 signifie que vous avez écrit quelque_chose = valeur (ou +=, -=…), alors que vous avez seulement le droit de lire ce quelque chose. Le plus souvent, c’est un champ de struct sur une variable sans mut, ou une valeur que vous atteignez à travers une référence &. C’est une erreur très courante, et la solution tient presque toujours en un mot : mut. Cette page montre d’abord la solution, puis l’explique. Pour les autres erreurs de cette série, consultez Les erreurs du compilateur Rust.

error[E0594]: cannot assign to `p.hp`, as `p` is not declared as mutable
--> src/main.rs:8:5
|
8 | p.hp = 5; // ❌ `p` est en lecture seule
| ^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
7 | let mut p = Player { name: String::from("Ferris"), hp: 10 };
| +++

Autrement dit (rustc s’exprime toujours en anglais) : impossible d’affecter une valeur à p.hp, car p n’est pas déclaré mutable ; le compilateur propose d’ajouter mut. Voici le programme qui la provoque :

struct Player {
name: String,
hp: u32,
}
fn main() {
let p = Player { name: String::from("Ferris"), hp: 10 };
p.hp = 5; // ❌ `p` est en lecture seule
println!("{} a {} PV", p.name, p.hp);
}

Vous verrez aussi souvent une deuxième formulation : « cannot assign to *x, which is behind a & reference » (impossible d’affecter une valeur à *x, qui est derrière une référence &). Même code d’erreur, cause un peu différente : là, vous tenez une référence &, et une référence & permet seulement de regarder. Les deux cas sont traités plus bas.

Regardez les ^^^^^^^^ sous p.hp = 5, ligne 8. Vous modifiez un champ de p. Mais p a été créé avec un simple let, donc toute la struct est en lecture seule, y compris chacun de ses champs.

La ligne help: montre la solution : let mut p. Les +++ marquent les trois caractères à ajouter.

Avec la deuxième formulation, le compilateur pointe une ligne comme *x += 1 et ajoute `x` is a `&` reference, so it cannot be written to (x est une référence &, on ne peut donc pas écrire à travers elle). Vous ne modifiez pas une variable que vous possédez. Vous essayez de modifier la valeur de quelqu’un d’autre à travers une référence empruntée, en lecture seule.

Pensez à un livre de bibliothèque. Vous pouvez le lire autant que vous voulez, mais vous n’avez pas le droit d’écrire dedans. Pour prendre des notes, il vous faut votre propre exemplaire (let mut) ou la permission du propriétaire (&mut).

En Rust, tout est en lecture seule, sauf si vous dites le contraire. Une variable a besoin de let mut pour changer, et une référence doit être &mut pour modifier ce qu’elle désigne. Voir Rendre une struct mutable et Les références mutables.

Il n’existe pas de « champ mutable » en Rust. La mutabilité appartient à la variable (ou à la référence), pas à des champs isolés. Donc let mut p rend modifiables tous les champs de p, et un simple let p n’en rend aucun modifiable. La règle reste simple : regardez comment une valeur a été déclarée, et vous savez si elle peut changer.

La règle des références vous protège des surprises. Si vous passez &player à une fonction, sa signature promet qu’elle va seulement lire. Sans cette promesse, n’importe quelle fonction pourrait modifier vos données en douce, et le compilateur ne pourrait pas empêcher un lecteur et un rédacteur de toucher la même valeur en même temps (c’est le sujet de E0502).

Choisissez cette solution quand vous possédez la struct et voulez modifier ses champs. C’est la bonne solution la plupart du temps.

struct Player {
name: String,
hp: u32,
}
fn main() {
let mut p = Player { name: String::from("Ferris"), hp: 10 };
p.hp = 5;
p.hp += 3;
println!("{} a {} PV", p.name, p.hp);
}

Sortie :

Ferris a 8 PV
▶ Essayez par vous-même

C’est pareil pour les tuples (pair.0 = 1;) et les tableaux (grid[2] = 7;) : la variable qui les contient a besoin de mut. Pour une struct dans un Vec, team[0].hp = 10; demande let mut team ; ce cas est signalé comme E0596, mais la solution est la même.

Si vous n’affectez qu’une seule fois, vous n’avez peut-être pas besoin de mut du tout : construisez la struct avec la bonne valeur dès le départ, par exemple Player { hp: 5, ..other }.

Choisissez cette solution quand vous parcourez une collection et voulez modifier chaque élément. for x in &v et v.iter() fournissent des références &, qui sont en lecture seule :

fn main() {
let scores = vec![10, 20, 30];
for x in &scores {
*x += 1; // ❌ `x` est un `&i32`
}
println!("{scores:?}");
}
error[E0594]: cannot assign to `*x`, which is behind a `&` reference
--> src/main.rs:4:9
|
3 | for x in &scores {
| ------- this iterator yields `&` references
4 | *x += 1; // ❌ `x` est un `&i32`
| ^^^^^^^ `x` is a `&` reference, so it cannot be written to
|
help: use a mutable iterator instead
|
3 | for x in &mut scores {
| +++

Le compilateur note que cet itérateur produit des références & et propose un itérateur mutable. Bouclez sur &mut scores (ou appelez .iter_mut()), et rendez le vecteur lui-même mut :

fn main() {
let mut scores = vec![10, 20, 30];
for x in &mut scores {
*x += 1;
}
println!("{scores:?}");
for x in scores.iter_mut() {
*x *= 2;
}
println!("{scores:?}");
}

Sortie :

[11, 21, 31]
[22, 42, 62]
▶ Essayez par vous-même

&mut scores et scores.iter_mut() font la même chose ; prenez celui qui se lit le mieux. Si vous n’avez pas besoin de modifier la liste sur place, en construire une nouvelle marche aussi : let bigger: Vec<i32> = scores.iter().map(|x| x + 1).collect();. Voir Les itérateurs.

Solution 3 : utiliser &mut self ou un paramètre &mut

Section intitulée « Solution 3 : utiliser &mut self ou un paramètre &mut »

Choisissez cette solution quand une méthode ou une fonction modifie des données qu’on lui a confiées. Une méthode avec &self peut seulement lire la struct, donc affecter une valeur à un champ échoue :

impl Account {
fn deposit(&self, amount: u64) {
self.balance += amount; // ❌ `self` est une référence `&`
}
}
error[E0594]: cannot assign to `self.balance`, which is behind a `&` reference
--> src/main.rs:7:9
|
7 | self.balance += amount;
| ^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to
|
help: consider changing this to be a mutable reference
|
6 | fn deposit(&mut self, amount: u64) {
| +++

Le compilateur explique que self est une référence & et propose d’en faire une référence mutable. Une fonction ordinaire avec un paramètre & donne la même erreur : fn reset(score: &u32) { *score = 0; } échoue avec cannot assign to `*score`, which is behind a `&` reference. La solution est la même dans les deux cas : écrivez &mut dans la signature, rendez la variable de l’appelant let mut, et passez &mut à l’appel.

struct Account {
balance: u64,
}
impl Account {
fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
}
fn reset(score: &mut u32) {
*score = 0;
}
fn main() {
let mut acc = Account { balance: 0 };
acc.deposit(50);
acc.deposit(25);
println!("Solde : {}", acc.balance);
let mut score = 42;
reset(&mut score);
println!("Score : {score}");
}

Sortie :

Solde : 75
Score : 0
▶ Essayez par vous-même

Pour les méthodes, Rust écrit le &mut à votre place lors de l’appel : acc.deposit(50) emprunte acc en mutable tout seul, du moment que acc est let mut. Pour les fonctions, c’est à vous de l’écrire : reset(&mut score). Si vous passez &score à une fonction qui attend un &mut u32, vous obtenez E0308 (types differ in mutability, les types diffèrent par leur mutabilité). Voir Ajouter du comportement avec des méthodes.

Choisissez cette solution seulement quand vous ne pouvez pas obtenir de &mut : par exemple, vous implémentez un trait dont la méthode est imposée en &self, ou plusieurs parties du programme partagent une même valeur via Rc. Cell et RefCell permettent de modifier à travers une référence & (c’est la « mutabilité intérieure ») :

use std::cell::{Cell, RefCell};
struct Catalog {
items: Vec<String>,
lookups: Cell<u32>,
history: RefCell<Vec<String>>,
}
impl Catalog {
fn contains(&self, name: &str) -> bool {
self.lookups.set(self.lookups.get() + 1);
self.history.borrow_mut().push(name.to_string());
self.items.iter().any(|item| item == name)
}
}
fn main() {
let catalog = Catalog {
items: vec![String::from("pomme"), String::from("poire")],
lookups: Cell::new(0),
history: RefCell::new(Vec::new()),
};
println!("poire ? {}", catalog.contains("poire"));
println!("kiwi ? {}", catalog.contains("kiwi"));
println!("Recherches : {}", catalog.lookups.get());
println!("Historique : {:?}", catalog.history.borrow());
}

Sortie :

poire ? true
kiwi ? false
Recherches : 2
Historique : ["poire", "kiwi"]
▶ Essayez par vous-même

Cell sert aux petites valeurs Copy comme les compteurs ; RefCell sert à tout le reste. Soyons honnêtes sur le prix à payer : RefCell déplace la vérification des emprunts de la compilation vers l’exécution, et il panique (panic) si vous empruntez deux fois en mutable en même temps. Si un simple &mut fonctionne, préférez-le. Entre plusieurs threads, utilisez plutôt Mutex ou les types atomiques (voir L’état partagé). Plus de détails dans RefCell.

Modifier une valeur dans une HashMap. *stock.get("apple").unwrap() += 1; échoue avec cannot assign to data in a `&` reference, parce que get renvoie une référence &. Solution : *stock.get_mut("apple").unwrap() += 1;, ou *stock.entry("apple").or_insert(0) += 1;. Voir Les HashMap.

Une closure passée comme Fn qui modifie une variable. run(|| count += 1), où run prend F: Fn(), échoue avec cannot assign to `count`, as it is a captured variable in a `Fn` closure (count est une variable capturée par une closure Fn). Solution : changez la contrainte en F: FnMut() (et mut f dans run). Voir Quand vous choisissez la mauvaise contrainte.

Un compteur global dans un static. static VISITS: u32 = 0; puis VISITS += 1; échoue avec cannot assign to immutable static item `VISITS`. Solution : utilisez static VISITS: AtomicU32 = AtomicU32::new(0); et VISITS.fetch_add(1, Ordering::Relaxed);, ou mieux, faites circuler un compteur en &mut.

  • E0596 : cannot borrow as mutable (impossible d’emprunter en mutable). Le même mut oublié, mais pour un appel de méthode comme list.push(x) au lieu d’une affectation.
  • E0384 : cannot assign twice to immutable variable. Vous affectez une valeur à une variable entière (x = 5;) plutôt qu’à un champ ou à travers une référence.
  • E0308 : mismatched types (types incompatibles), types differ in mutability, quand vous passez &x là où &mut x est attendu.
  • E0502 : vous avez un accès &mut, mais quelque chose d’autre lit encore la valeur.
  • E0507 : cannot move out of a value behind a reference (impossible de déplacer une valeur derrière une référence). La règle de la « lecture seule » encore, cette fois pour un déplacement au lieu d’une écriture.
  • Toutes les erreurs du compilateur : la liste complète des pages d’erreurs.

Petit quiz

1. let user = User { age: 30 }; user.age = 31; donne E0594. Quelle est la solution ?

2. for p in &prices { *p += 1; } échoue. Que changez-vous ?

3. Une méthode fn rename(&self, n: String) { self.name = n; } donne E0594. Quelle est la solution habituelle ?

Score : 0 / 3