起步:先把地基和你会的那门语言对齐
可变性、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` 就过了 —— 那是在告诉编译器「这个函数可以在编译期跑」。