起步:先把地基和你会的那门语言对齐

可变性、shadowing 与常量

它在解决什么

mut 看起来只是个关键字,但它是所有权那一章的前置概念: 借用规则里那条「同时只能有一个可变借用」,第一步就要求你先分清 什么东西是可变的、可变的到底是谁。

这一篇把这一个点从五个角度打透,然后顺带收掉 shadowing 和 const / static。

mut 修饰的是绑定

不是类型,不是值,是那个名字。

let s = String::from("a");       // s 这个名字不能改
let mut t = String::from("a");   // t 可以

「可以改」包含两件事,而 mut 一个字同时管了:

操作 例子 不写 mut 时报
改内容 v.push(3) E0596
整个换掉 v = vec![9] E0384

这两个码会同时出现,因为那段对照项两件事都干了。

mut 和 &mut 是两件事

这一对最容易混:

let mut s = ...;      // 声明:这个名字后面还能改
let r = &mut s;       // 借用:我借来改

后者要求前者先成立 —— 对一个不可变绑定取 &mut 就是 E0596。

⇒ 记法:mut 是所有者说的话,&mut 是借用者说的话。 借用者能不能借到,取决于所有者有没有那个权限。

没有「某个字段可变」这回事

结构体定义里不能给单个字段加 mut。可变性整个挂在绑定上:

let p = P { x: 1 };
let mut q = P { x: 1 };
q.x = 2;        // 行
p.x = 2;        // E0594

实测就是这样。

⇒ 想要「整体只读、某个字段可改」,得用内部可变性(Cell / RefCell), 见《智能指针与内部可变性》 —— 它的代价是把借用检查从编译期挪到运行期。

shadowing:不是「改变量」,是再声明一个

let x = "42";
let x = x.parse::<i32>().unwrap();
let x = x * 2;

这三个 x 是三个不同的变量,只是名字一样。 所以类型能从 &str 变成 i32。

换成 let mut x 就不行了(E0308)—— 一个变量的类型定下来就不能变。

⇒ 判据很清楚:

你想干什么 用哪个
同一个东西,值要变 let mut
转换 / 解析之后换个类型,但名字懒得换 shadowing

📌 shadowing 最典型的用法就是上面那种「字符串 → 解析 → 校验后的值」的流水线。 它让中间那些名字(x_str、x_parsed、x_checked)全都不必存在。

⚠️ 但别在同一个作用域里隔很远的地方 shadow 同一个名字 —— 那时读者已经忘了上面那个是什么类型了。挨着写的才好读。

const 与 static

两个都是「全局的、不可变的」,差别在有没有固定的内存地址:

const static
语义 内联到每个使用处 有一个固定地址,整个程序共享
生命周期 没有地址可言 'static
能不能 mut 不能 能,但要 unsafe
什么时候用 几乎总是它 需要地址、或者很大的数据

⇒ 判据:默认用 const。 只有在「这个东西必须只有一份、要取它的地址」 或者「它大到不该被内联很多次」时才用 static。

两者的值都必须在编译期算得出来。 用一个普通函数去初始化 const 就是 E0015:

fn compute() -> u32 { 100_000 }
const MAX: u32 = compute();      // E0015

⚠️ 把 fn 改成 const fn 就过了 —— 那是在告诉编译器「这个函数可以在编译期跑」。 标准库里很多构造函数都是 const fn,所以 const EMPTY: Vec<i32> = Vec::new(); 这种写法是合法的。

📌 命名习惯:const 和 static 一律 SCREAMING_SNAKE_CASE, 而且必须写类型标注 —— 它们不参与类型推断。

为什么默认不可变

这是个设计取舍,值得说一句:默认不可变让**「这个值会不会变」成为签名的一部分**。

看到 fn f(x: &Foo) 你就知道 f 不会改它,不用点进去看; 看到 &mut Foo 就知道它会。在默认可变的语言里,这件事只能靠文档和信任。

而代价是你得多打四个字符,以及在刚上手时被编译器提醒很多次 「这里需要 mut」—— 那些提醒在两周之后就基本不会再出现了。

这一章到这里就完整了

篇 它回答的问题
为什么是 Rust 它在哪几个地方做了不同的选择,代价是什么
Cargo 与工程骨架 工具怎么用,版本怎么钉死
语法速通 和你会的那门语言不一样的语法
本篇 可变性挂在谁身上

下一步

地基铺完了。接下来是这套教程真正的主体,也是 Rust 唯一真正难的地方 —— 所有权:《移动:赋值为什么会让变量失效》。

全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

`mut` 修饰的是绑定 —— 没写就连借都借不出去

fn main() {
    let mut s = String::from("a");
    s.push('b');
    let t = String::from("c");
    let r = &mut s;
    r.push('d');
    println!("{s} {t}");
}

编译通过 · 输出 "abd c\n"

对照:改成对不可变的 `t` 取 `&mut`
fn main() {
    let mut s = String::from("a");
    s.push('b');
    let t = String::from("c");
    let r = &mut t;
    r.push('d');
    println!("{s} {t}");
}

编译不过:error[E0596]

对照项报 `E0596`:`t` 这个绑定不是 `mut` 的,就没法从它借出一个可变引用。⇒ **`mut` 和 `&mut` 是两件事**:前者说「这个名字后面还能改」,后者说「我借来改」。后者要求前者先成立。

shadowing 不是「改变量」—— 它能换类型

fn main() {
    let x = "42";
    let x = x.parse::<i32>().unwrap();
    let x = x * 2;
    println!("{x}");
}

编译通过 · 输出 "84\n"

对照:改用 `let mut x` 加两次赋值
fn main() {
    let mut x = "42";
    x = x.parse::<i32>().unwrap();
    x = x * 2;
    println!("{x}");
}

编译不过:error[E0308]、error[E0369]

三个 `x` 是**三个不同的变量**,只是名字一样 —— 所以类型可以从 `&str` 变成 `i32`。对照项用 `mut` 就不行了(`E0308`):一个变量的类型定下来就不能变。⇒ 「解析 / 转换之后名字不用换」是 shadowing 最常见的正当用途。

「改内容」和「整个换掉」都要 `mut`

fn main() {
    let mut v = vec![1, 2];
    v.push(3);
    v = vec![9];
    println!("{v:?}");
}

编译通过 · 输出 "[9]\n"

对照:去掉 `mut`
fn main() {
    let v = vec![1, 2];
    v.push(3);
    v = vec![9];
    println!("{v:?}");
}

编译不过:error[E0596]、error[E0384]

对照项报 `E0596`(`push` 要可变借用)和 `E0384`(不能给不可变变量二次赋值)—— 两个码正好对应「改内容」和「整个换掉」两种操作。`mut` 一个字同时管这两件事。

没有「某个字段可变」这回事

struct P {
    x: i32,
}

fn main() {
    let p = P { x: 1 };
    let mut q = P { x: 1 };
    q.x = 2;
    println!("{} {}", p.x, q.x);
}

编译通过 · 输出 "1 2\n"

对照:给不可变的 `p` 的字段赋值
struct P {
    x: i32,
}

fn main() {
    let p = P { x: 1 };
    let mut q = P { x: 1 };
    p.x = 2;
    println!("{} {}", p.x, q.x);
}

编译不过:error[E0594]

结构体定义里**不能给单个字段加 `mut`** —— 可变性整个挂在绑定上。对照项报 `E0594`。⇒ 想要「大部分只读、某个字段可改」,得用内部可变性(`Cell` / `RefCell`),那是进阶那一章的事。

`const` 的值必须在编译期算得出来

const MAX: u32 = 100_000;
static NAME: &str = "demo";

fn main() {
    println!("{MAX} {NAME}");
}

编译通过 · 输出 "100000 demo\n"

对照:把 `MAX` 改成调用一个普通函数
fn compute() -> u32 {
    100_000
}

const MAX: u32 = compute();
static NAME: &str = "demo";

fn main() {
    println!("{MAX} {NAME}");
}

编译不过:error[E0015]

对照项报 `E0015`:`const` 只能用编译期能求值的表达式(字面量、`const fn`、其它 `const`)。⚠️ 把 `fn compute` 改成 `const fn compute` 就过了 —— 那是在告诉编译器「这个函数可以在编译期跑」。