跳转到内容

Rust 错误 E0384:cannot assign twice to immutable variable(给不可变变量二次赋值)

E0384 的意思是:你给一个变量赋了值,接着又想给它另一个值,可这个变量声明时没有加 mut。在 Rust 里,除非你明确说要改,否则变量是不能修改的。几乎每个人在学 Rust 的第一个小时里都会碰到它,而修复通常只需要三个字母:mut。本页先给出修法,再解释 Rust 为什么这样设计。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0384]: cannot assign twice to immutable variable `score`
--> src/main.rs:4:5
|
2 | let score = 10;
| ----- first assignment to `score`
3 | println!("Score: {score}");
4 | score = 15; // ❌ score 没有声明 mut
| ^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut score = 10;
| +++

第一行的意思是“不能给不可变变量 score 赋值两次”。help 建议你“把这个绑定改成可变的”。

产生这个错误的程序是这样的:

fn main() {
let score = 10;
println!("Score: {score}");
score = 15; // ❌ score 没有声明 mut
println!("New score: {score}");
}

如果这个变量是函数参数,措辞会稍有不同:“cannot assign to immutable argument”(不能给不可变参数赋值)。错误代码一样,道理也一样。

编译器指出了两个位置:

  1. 第 2 行,标着 first assignment to score(第一次给 score 赋值):score 在这里得到了它的值。因为这一行写的是 let 而不是 let mut,这个值就定下来了。
  2. 第 4 行,用 ^^^^ 标出:你在这里想放进一个新值,Rust 拒绝了。

help: 部分已经给出了修法:let mut score = 10;。+++ 标出了要加上的三个字符。

可以把 let 想成用钢笔写字,把 let mut 想成用铅笔写字。两种都没问题,但用钢笔写下的东西,之后就擦不掉了。

同样的规则也适用于 +=、-=、*= 这类运算符。count += 1 也是一次赋值,所以同样需要 mut。

在 Rust 里,所有变量默认都是不可变的。想要修改,必须用 mut 主动开启。变量那一页讲过这一点。

这听起来很严格,但对你帮助很大。读到 let total = ... 时,你就知道 total 会一直保持这个值,直到作用域结束。你不用把函数剩下的部分翻一遍,确认有没有什么地方改了它。而看到 let mut,就是一个清楚的信号:“注意这个,它会变”。

它还能抓出真正的 bug。如果你本想修改一个副本,却不小心写到了原来的变量上,或者不小心重复用了一个名字,编译器都会拦住你。而且对于从来没有被修改过的 mut 变量,Rust 也会发出警告(variable does not need to be mutable,变量不需要是可变的),所以这个信号在两个方向上都是可信的。

当变量确实需要随时间变化时,就用这个方法:计数器、累计总数、分数、状态等。大多数情况下这都是正确的修法,也正是编译器的建议。

fn main() {
let mut score = 10;
println!("分数:{score}");
score = 15; // ✅ 现在可以了
println!("新分数:{score}");
let mut count = 0;
for _ in 0..3 {
count += 1;
}
println!("计数:{count}");
}

输出:

分数:10
新分数:15
计数:3
▶ 动手试一试

计数器是最典型的例子:先写 let count = 0;,再在循环里写 count += 1;,就会在 += 那一行得到 E0384。加上 mut 就好了。参见用 mut 让变量可以修改。

当你其实并不想修改这个变量时,就用这个方法。你想要的是一个从旧值算出来的新值,往往要分几步算,算完之后就不再变了。用同一个名字再写一次 let:

fn main() {
let price = 20;
let price = price + 5; // 一个新的 `price`,由旧的算出来
let price = price * 2;
println!("最终价格:{price}");
let input = " 42 ";
let input = input.trim(); // 仍然是文本
let input: u32 = input.parse().unwrap(); // 现在是数字了
println!("解析结果:{}", input + 1);
}

输出:

最终价格:50
解析结果:43
▶ 动手试一试

这叫做遮蔽(shadowing):每个 let 都会创建一个全新的变量,把旧的那个遮住。这样做有两个好处。最后的 price 是不可变的,之后不会被意外修改。而且类型可以改变,比如 input 从 &str 变成了 u32,这是 mut 做不到的。更多内容见遮蔽:重复使用同一个名字。

一个简单的规则:如果值会一次又一次地变化(在循环里、随着时间),就用 mut。如果你只是在分几步准备一个值,用遮蔽读起来更清楚。

当函数想修改它自己那份参数时,就用这个方法。参数默认也是不可变的,所以下面的代码会失败:

fn countdown(n: u32) {
while n > 0 {
println!("{n}...");
n -= 1; // ❌ 参数 n 不可变
}
println!("Liftoff!");
}
error[E0384]: cannot assign to immutable argument `n`
--> src/main.rs:4:9
|
4 | n -= 1; // ❌ 参数 n 不可变
| ^^^^^^ cannot assign to immutable argument
|
help: consider making this binding mutable
|
1 | fn countdown(mut n: u32) {
| +++

在参数名前面加上 mut:

fn countdown(mut n: u32) {
while n > 0 {
println!("{n}...");
n -= 1;
}
println!("发射!");
}
fn main() {
let start = 3;
countdown(start);
println!("start 仍然是 {start}");
}

输出:

3...
2...
1...
发射!
start 仍然是 3
▶ 动手试一试

注意,main 里的 start 并没有变。mut n 只是让函数自己那份副本可以修改;调用方不需要 mut,也不受影响。如果你想让函数修改调用方的变量,就改成接收 &mut u32,并传入 &mut start。参见函数和借用。

修改 for 循环变量。 每一轮循环,循环变量都是一个全新的不可变变量:

for i in 0..5 {
if i == 2 {
i += 1; // ❌ cannot assign twice to immutable variable `i`
}
println!("{i}");
}

修复:你很少真的需要修改循环计数器本身。可以用 continue 跳过,用 .step_by(2),或者如果需要完全掌控,改用带 let mut i 的 while 循环。(for mut i in 0..5 能编译,但它只会修改这一轮的副本,并不会跳过任何东西。)参见 break 和 continue 以及 while 循环。

先声明、后赋值。 不带值的 let total; 是允许的,你可以给它赋值一次。第二次赋值就会得到 E0384:

let total;
total = 5;
total = 6; // ❌ cannot assign twice to immutable variable `total`

修复:如果它真的会变,就用 let mut total。如果你只是想从几个值里选一个,就用一个表达式算出来:let total = if big_order { 6 } else { 5 };。参见 if / else。

累加总数。 先写 let sum = 0;,再在循环里写 sum += price;,这是最常见的 E0384。修复:let mut sum = 0;,或者干脆不要这个变量,写成 let sum: u32 = prices.iter().sum();。参见迭代器。

  • E0596:cannot borrow as mutable, as it is not declared as mutable(变量没有声明为可变,不能可变借用)。同样是“忘了 mut”,只不过出现在调用 .push() 这类会修改值的方法时,而不是赋值时。
  • E0594:cannot assign to p.hp, as p is not declared as mutable。当你给结构体字段赋值(p.hp = 5;)或通过 & 引用写入时会看到它。修复:let mut p,或者用 &mut 引用。参见让结构体可变。
  • E0502:值正被借用来读取时,你又想修改它。
  • E0382:值被移动之后,你还在使用它。
  • 所有编译错误:错误页面的完整列表。

小测验

1. 你写了 let lives = 3;,后来又写了 lives -= 1;。为什么会得到 E0384?

2. 你想把 name 整理一次(去掉首尾空格并转成大写),之后再也不改它。哪种写法最清楚?

3. fn halve(n: u32) 里写了 n /= 2;,报了 E0384。不影响调用方的修法是什么?

得分: 0 / 3