Rust 错误 E0594:cannot assign, as it is not declared as mutable(给非 mut 数据赋值)
E0594 的意思是:你写了 某个东西 = 值(或者 +=、-= 等等),可你对这个东西只有读的权限。通常它是一个没有 mut 的变量上的结构体字段,或者是你通过 & 引用拿到的值。这个错误非常常见,修复几乎总是一个词:mut。这一页先给出修复办法,再解释原因。这个系列里的其他错误,见 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` is read-only | ^^^^^^^^ cannot assign |help: consider changing this to be mutable |7 | let mut p = Player { name: String::from("Ferris"), hp: 10 }; | +++下面这段程序会产生这个错误:
struct Player { name: String, hp: u32,}
fn main() { let p = Player { name: String::from("Ferris"), hp: 10 }; p.hp = 5; // ❌ `p` is read-only println!("{} has {} hp", p.name, p.hp);}(注释保留英文,因为 rustc 会把这一行原样打印在错误信息里,意思是“p 是只读的”。)
你还会经常看到另一种说法:“cannot assign to *x, which is behind a & reference”(不能给 *x 赋值,因为它在一个 & 引用后面)。错误代码相同,原因略有不同:这时你手里拿的是一个 & 引用,而 & 引用只能看。下面两种情况都会讲到。
这个错误是什么意思
Section titled “这个错误是什么意思”看第 8 行 p.hp = 5 下面的 ^^^^^^^^。你在修改 p 的一个字段。可是 p 是用普通的 let 创建的,所以整个结构体都是只读的,里面的每个字段也一样。
help: 这一行给出了修复:let mut p。+++ 标出了要加的那三个字符。
如果是第二种说法,编译器会指向 *x += 1 这样的一行,并加上 `x` is a `&` reference, so it cannot be written to(x 是一个 & 引用,所以不能写入)。你改的不是自己拥有的变量,而是想通过一个借来的、只读的引用,去改别人的值。
想象一本图书馆的书。你想读多少遍都行,但不能在上面写字。想做笔记,你得有一本自己的(let mut),或者得到主人的允许(&mut)。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”在 Rust 里,除非你特别说明,一切都是只读的。变量要写成 let mut 才能改,引用要是 &mut 才能改它指向的东西。见 让结构体可变 和 可变引用。
Rust 里没有“可变字段”这种东西。可变性属于变量(或者引用),而不属于单个字段。所以 let mut p 让 p 的每个字段都能改,普通的 let p 则一个都不能改。这让规则保持简单:看一个值是怎么声明的,就知道它能不能改。
引用的规则能保护你不被意外打扰。如果你把 &player 传给一个函数,它的签名就承诺了只会读。没有这个承诺,任何函数都可能悄悄改掉你的数据,编译器也没法阻止一个读者和一个写者同时碰同一个值(这正是 E0502 讲的事)。
修复 1:给变量加上 mut
Section titled “修复 1:给变量加上 mut”如果结构体归你所有,你想修改它的字段,就选这个办法。大多数时候这都是正确的修复。
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!("{} 还剩 {} 点生命值", p.name, p.hp);}输出:
Ferris 还剩 8 点生命值元组(pair.0 = 1;)和数组(grid[2] = 7;)也一样:存放它们的变量需要 mut。对于 Vec 里的结构体,team[0].hp = 10; 需要 let mut team;这种情况报的是 E0596,不过修复方法相同。
如果只赋值一次,你可能根本不需要 mut:一开始就用正确的值构造结构体,比如 Player { hp: 5, ..other }。
修复 2:用 &mut v 或 iter_mut() 循环
Section titled “修复 2:用 &mut v 或 iter_mut() 循环”如果你在遍历一个集合,想修改其中的每一项,就选这个办法。for x in &v 和 v.iter() 给出的是 & 引用,它们是只读的:
fn main() { let scores = vec![10, 20, 30]; for x in &scores { *x += 1; // ❌ `x` is a `&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 `&` references4 | *x += 1; // ❌ `x` is a `&i32` | ^^^^^^^ `x` is a `&` reference, so it cannot be written to |help: use a mutable iterator instead |3 | for x in &mut scores { | +++改成遍历 &mut scores(或者调用 .iter_mut()),并且让 vector 本身也是 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:?}");}输出:
[11, 21, 31][22, 42, 62]&mut scores 和 scores.iter_mut() 做的是同一件事,哪个读起来顺眼就用哪个。如果不需要原地修改列表,造一个新的也可以:let bigger: Vec<i32> = scores.iter().map(|x| x + 1).collect();。见 迭代器。
修复 3:用 &mut self 或 &mut 参数
Section titled “修复 3:用 &mut self 或 &mut 参数”如果是方法或函数修改了传给它的数据,就选这个办法。带 &self 的方法只能读结构体,所以给字段赋值会失败:
impl Account { fn deposit(&self, amount: u64) { self.balance += amount; // ❌ `self` 是一个 `&` 引用 }}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) { | +++带 & 参数的普通函数也会报同样的错:fn reset(score: &u32) { *score = 0; } 会报 cannot assign to `*score`, which is behind a `&` reference。两者的修复方法一样:在签名里写 &mut,让调用者的变量是 let mut,调用时传 &mut。
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!("余额:{}", acc.balance);
let mut score = 42; reset(&mut score); println!("分数:{score}");}输出:
余额:75分数:0对于方法,Rust 会在调用处替你写好 &mut:只要 acc 是 let mut,acc.deposit(50) 就会自动可变地借用 acc。对于函数,你得自己写:reset(&mut score)。如果把 &score 传给一个要 &mut u32 的函数,会得到 E0308(types differ in mutability,可变性不同)。见 用方法添加行为。
修复 4(最后的办法):Cell 或 RefCell
Section titled “修复 4(最后的办法):Cell 或 RefCell”只有在你拿不到 &mut 的时候才选这个办法:比如你在实现一个 trait,它的方法签名固定是 &self;或者程序的很多部分通过 Rc 共享同一个值。Cell 和 RefCell 允许通过 & 引用修改值(“内部可变性”):
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("apple"), String::from("pear")], lookups: Cell::new(0), history: RefCell::new(Vec::new()), }; println!("有 pear 吗?{}", catalog.contains("pear")); println!("有 kiwi 吗?{}", catalog.contains("kiwi")); println!("查询次数:{}", catalog.lookups.get()); println!("查询记录:{:?}", catalog.history.borrow());}输出:
有 pear 吗?true有 kiwi 吗?false查询次数:2查询记录:["pear", "kiwi"]Cell 适合计数器这样的小 Copy 值;其他情况用 RefCell。代价要说清楚:RefCell 把借用检查从编译时挪到了运行时,如果你同时可变借用两次,它会 panic。能用普通的 &mut 就优先用它。跨线程时,改用 Mutex 或原子类型(见 共享状态)。更多内容见 RefCell。
修改 HashMap 里的值。 *stock.get("apple").unwrap() += 1; 会报 cannot assign to data in a `&` reference(不能给 & 引用里的数据赋值),因为 get 返回的是 & 引用。修复:*stock.get_mut("apple").unwrap() += 1;,或者 *stock.entry("apple").or_insert(0) += 1;。见 HashMap。
作为 Fn 传入、却修改了变量的闭包。 run 接收 F: Fn() 时,run(|| count += 1) 会报 cannot assign to `count`, as it is a captured variable in a `Fn` closure(不能给 count 赋值,因为它是 Fn 闭包捕获的变量)。修复:把约束改成 F: FnMut()(并在 run 里写成 mut f)。见 约束选错了会怎样。
放在 static 里的全局计数器。 写了 static VISITS: u32 = 0; 再写 VISITS += 1;,会报 cannot assign to immutable static item `VISITS`(不能给不可变的 static 项 VISITS 赋值)。修复:用 static VISITS: AtomicU32 = AtomicU32::new(0); 和 VISITS.fetch_add(1, Ordering::Relaxed);;更好的做法是用 &mut 把计数器传来传去。
- E0596:cannot borrow as mutable(不能可变借用)。同样是缺了
mut,只不过出现在list.push(x)这样的方法调用上,而不是赋值上。 - E0384:cannot assign twice to immutable variable(不能给不可变变量赋值两次)。你赋值的是整个变量(
x = 5;),而不是字段,也不是通过引用。 - E0308:mismatched types(类型不匹配),
types differ in mutability,在需要&mut x的地方传了&x时出现。 - E0502:你有
&mut访问权,但还有别的地方在读这个值。 - E0507:cannot move out of a value behind a reference(不能从引用后面的值中移出)。又是“只读”规则,这次针对的是移动而不是写入。
- 所有编译错误:全部错误页面的列表。
小测验
1. let user = User { age: 30 }; user.age = 31; 报 E0594。怎么修复?
2. for p in &prices { *p += 1; } 报错了。要改什么?
3. 方法 fn rename(&self, n: String) { self.name = n; } 报 E0594。通常怎么修复?
得分: 0 / 3
- E0594 — Rust error codes index(英文):这个错误的官方说明。
- 结构体的定义和实例化 —《Rust 程序设计语言》:为什么必须让整个实例可变,而不能只让某个字段可变。
- 引用与借用 —《Rust 程序设计语言》:其中“可变引用”一节讲解了
&和&mut的区别。 - 方法语法 —《Rust 程序设计语言》:方法什么时候需要
&self、&mut self或self。 RefCell<T>与内部可变性模式 —《Rust 程序设计语言》:RefCell怎样允许通过&修改值,以及它在运行时的代价。