跳转到内容

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 赋值,因为它在一个 & 引用后面)。错误代码相同,原因略有不同:这时你手里拿的是一个 & 引用,而 & 引用只能看。下面两种情况都会讲到。

看第 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 里,除非你特别说明,一切都是只读的。变量要写成 let mut 才能改,引用要是 &mut 才能改它指向的东西。见 让结构体可变 和 可变引用。

Rust 里没有“可变字段”这种东西。可变性属于变量(或者引用),而不属于单个字段。所以 let mut p 让 p 的每个字段都能改,普通的 let p 则一个都不能改。这让规则保持简单:看一个值是怎么声明的,就知道它能不能改。

引用的规则能保护你不被意外打扰。如果你把 &player 传给一个函数,它的签名就承诺了只会读。没有这个承诺,任何函数都可能悄悄改掉你的数据,编译器也没法阻止一个读者和一个写者同时碰同一个值(这正是 E0502 讲的事)。

如果结构体归你所有,你想修改它的字段,就选这个办法。大多数时候这都是正确的修复。

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 `&` references
4 | *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();。见 迭代器。

如果是方法或函数修改了传给它的数据,就选这个办法。带 &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