bigywater.tech
Rust · 3

变量默认不可变

「变量默认不可变」是 Rust 最先遇到的规则。这篇把规则逐条变成编译器的原话:哪些写法会报什么错、错误提示怎么指出两次赋值的位置、延迟初始化到底允许什么,最后用汇编确认 mut 这个修饰词在运行时一点代价都没有。

E0384
给不可变变量重复赋值的错误码
深
不可变连字段赋值(E0594)与 &mut self 方法(E0596)都挡住
0 开销
let 与 let mut 的函数 -O 后机器码相同(合并成别名)

默认不可变

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let x = 5;
      3 |     println!("x = {x}");
      4 |     x = 6;
      5 |     println!("x = {x}");
      6 | }
  编译:失败
    error[E0384]: cannot assign twice to immutable variable `x`
     --> <实验目录>/src/u3a.rs:4:5
      |
    2 |     let x = 5;
      |         - first assignment to `x`
    3 |     println!("x = {x}");
    4 |     x = 6;
      |     ^^^^^ cannot assign twice to immutable variable
      |
    help: consider making this binding mutable
      |
    2 |     let mut x = 5;
      |         +++
    
    error: aborting due to 1 previous error
    
    For more information about this error, try `rustc --explain E0384`.

注意编译器给的修复建议(consider making this binding mutable)—— 但是否该这么改,要看你为什么要重新赋值。很多时候新建一个变量(见Shadowing 与 mut 的区别)更合适。

加上 mut

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let mut x = 5;
      3 |     println!("x = {x}");
      4 |     x = 6;
      5 |     println!("x = {x}");
      6 |     let y = x;      // 把 mut 变量的值赋给不可变变量:允许
      7 |     println!("y = {y}");
      8 | }
  编译:成功
  运行(退出码 0):
    x = 5
    x = 6
    y = 6

不可变是深的

实测 · 本机执行输出
  源码:
      1 | struct P { a: i32 }
      2 | fn main() {
      3 |     let p = P { a: 1 };
      4 |     p.a = 2;
      5 |     let v = Vec::new();
      6 |     v.push(1);
      7 |     println!("{} {}", p.a, v.len());
      8 | }
  编译:失败
    error[E0594]: cannot assign to `p.a`, as `p` is not declared as mutable
     --> <实验目录>/src/u3c.rs:4:5
      |
    4 |     p.a = 2;
      |     ^^^^^^^ cannot assign
      |
    help: consider changing this to be mutable
      |
    3 |     let mut p = P { a: 1 };
      |         +++
    
    error[E0596]: cannot borrow `v` as mutable, as it is not declared as mutable
     --> <实验目录>/src/u3c.rs:6:5
      |
    6 |     v.push(1);
      |     ^ cannot borrow as mutable
      |
    help: consider changing this to be mutable
      |
    5 |     let mut v = Vec::new();
      |         +++
    
    error: aborting due to 2 previous errors
    
    Some errors have detailed explanations: E0594, E0596.
    For more information about an error, try `rustc --explain E0594`.

字段赋值与 push 为什么要求 mut,与借用规则有关:push 需要 &mut self,而要拿到可变借用,绑定必须是 mut 的(见不可变借用与可变借用)。

延迟初始化

实测 · 本机执行输出
  源码:
      1 | fn main() {
      2 |     let x;
      3 |     let flag = std::env::args().count() > 1;
      4 |     if flag { x = 1; } else { x = 2; }
      5 |     println!("x = {x}");
      6 | }
  编译:成功
  运行(退出码 0):
    x = 2
  源码:
      1 | fn main() {
      2 |     let x;
      3 |     x = 1;
      4 |     x = 2;
      5 |     println!("x = {x}");
      6 | }
  编译:失败
    error[E0384]: cannot assign twice to immutable variable `x`
     --> <实验目录>/src/u3e.rs:4:5
      |
    3 |     x = 1;
      |     ----- first assignment to `x`
    4 |     x = 2;
      |     ^^^^^ cannot assign twice to immutable variable
      |
    help: consider making this binding mutable
      |
    2 |     let mut x;
      |         +++
    
    warning: value assigned to `x` is never read
     --> <实验目录>/src/u3e.rs:3:5
      |
    3 |     x = 1;
      |     ^^^^^ this value is reassigned later and never used
    4 |     x = 2;
      |     ----- `x` is overwritten here before the previous value is read
      |
      = note: `#[warn(unused_assignments)]` (part of `#[warn(unused)]`) on by default
    
    error: aborting due to 1 previous error; 1 warning emitted
    …(共 26 行)

机器码里没有 mut

实测 · 本机执行输出
  imm  的汇编: mov	w8, #6 ; mov	w9, #2 ; madd	w0, w0, w8, w9 ; ret
  mutv 的汇编: (没有单独的函数体:汇编里是一行 `_mutv = _imm`,LLVM 发现两个函数完全相同,把 mutv 合并成了 imm 的别名)
  两个函数的机器码相同: True
  (Rust 的可变性检查全在编译期的借用检查阶段,生成的机器码里没有 mut 的痕迹)

这是「零成本抽象」的一个最小例子(见Zero-Cost Abstraction)。

分步演示

不可变与 mut
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。编译器报错是原样的 rustc 1.98.1 输出(路径已替换为 <实验目录>);汇编用 --emit=asm -O 生成,目标 aarch64-apple-darwin
    • 没有验证:其它目标平台(x86_64)上的汇编、debug 构建里 let 与 let mut 的 MIR 差别、模式匹配里 mut 绑定、闭包捕获时的可变性
    京ICP备2026067502号-1京公网安备11010502063487号