Rust 的变量、可变性与遮蔽
Rust 的 let 绑定默认不可变;mut 显式可变;shadowing 用同名新绑定重定义;与 const 的边界。
#type / concept
#status / growing
#tech / dev
#resource / rust
[!info] 关联笔记
Rust 的变量、可变性与遮蔽
这个概念为什么出现
写出第一个有状态的程序前,必须回答:
- 名字如何绑定到值?
- 为什么默认不能改?
- 想改时如何显式声明?
- “重新用同名变量”是改旧值,还是新建绑定?
- 常量
const与不可变变量有何不同?
Rust 的答案是:默认不可变绑定 + 显式 mut + 遮蔽(shadowing)。这为后续所有权与并发安全打底——能变的状态越少,推理越简单。
[!abstract] 一句话理解
let创建默认不可变绑定;let mut允许通过该绑定写入;再次let 同名是遮蔽(可改类型),不是mut原地改值;const是编译期常量,不是运行时可绑可变存储。
最小可运行示例
场景:订单服务处理一次优惠券改写与审计字段升级
结账流水线里有三段状态:
- 商品件数:处理过程中会改(需要
mut) - 客户显示名:从原始字符串“净化”成新字符串(常用 shadowing,甚至类型从
&str到String) - 服务名常量:全进程编译期固定(
const)
// 业务意图:
// 1. 可变计数:加购一件;
// 2. 遮蔽:把输入名规范化成拥有型 String;
// 3. 常量:服务标识不可变。
// 教学点:
// - 默认不可变;
// - mut 改的是绑定指向的值;
// - shadowing 是新绑定,可换类型。
const SERVICE_NAME: &str = "order-api";
fn normalize_customer_name(raw: &str) -> String {
// 遮蔽:先绑定 trim 后的 &str 视图,再绑定为拥有的 String
let raw = raw.trim();
let raw = raw.to_ascii_lowercase();
raw
}
fn main() {
println!("service={}", SERVICE_NAME);
// 件数会变:显式 mut
let mut item_count = 2;
item_count += 1; // 加购
println!("item_count={}", item_count);
// 不可变绑定:下面若取消注释 item_count 式写法会失败
let order_id = 1001;
// order_id = 1002; // 编译错误:cannot assign twice to immutable variable
let customer = normalize_customer_name(" Ada ");
println!("order_id={} customer={}", order_id, customer);
}
建议运行:
cargo run
期望输出:
service=order-api
item_count=3
order_id=1001 customer=ada
结合场景再看三个关注点
- 可变性是绑定的属性:
mut说的是“能不能通过这个名字写”,与“值是否 Copy”是不同轴。 - shadowing 适合变换流水线:
trim → lowercase → owned,每一步一个更精确的绑定。 - const 放配置真相:服务名/数学常数,而不是“懒得写 mut 的变量”。
核心概念与准确模型
1. 绑定(binding)
let x = 5;
- 引入名字
x - 默认不可变:不能
x = 6 - 类型可推断,也可注解:
let x: i32 = 5;
2. 可变绑定
let mut x = 5;
x = 6;
- 允许通过
x重新赋值(类型仍须一致) - 可变性帮助编译器与读者标记“这里存在状态”
3. 遮蔽(shadowing)
let x = 5;
let x = x + 1; // 新绑定,旧 x 被遮住
let x = "five"; // 还可以换类型
mut 赋值 | shadowing | |
|---|---|---|
| 关键字 | let mut 后 = | 再次 let |
| 能否改类型 | 否 | 是 |
| 是否新绑定 | 否(写同一绑定) | 是 |
| 典型用途 | 计数器、缓冲填充 | 变换、解析、收窄语义 |
4. 作用域
内层 shadowing 不影响外层同名绑定在离开内层后的恢复:
let x = 1;
{
let x = 2;
println!("{x}"); // 2
}
println!("{x}"); // 1
5. const 与 static(入门边界)
const:编译期常量,必须注解类型,可内联到使用处static:具有'static生命周期的固定内存位置(全局状态需更谨慎,并发章节再深挖)- 不可变
let仍是运行时绑定,不是const
设计动机
- 默认不可变降低意外状态扩散,契合“共享 xor 可变”的后续借用规则
- 显式 mut 让代码审查时状态写入可见
- shadowing 允许函数式变换风格,而不强制全程
mut+ 可改类型尴尬
边界情况与反直觉行为
let mut s = String::from("a"); s.push('b');:改的是String内容,绑定仍是那个s。- 不可变绑定可以“内部可变”(
RefCell等)——这是进阶主题,见 内部可变性。 - move 之后不能用不是因为“不可变”,而是所有权转移;见 所有权。
常见误区
[!warning] 常见误区:把 shadowing 当成 mut 错误理解:
let x = x + 1是在改旧变量。
正确模型:创建新绑定,旧绑定被遮住。
为何易错:其他语言里同名再声明规则不同。
[!warning] 常见误区:为了省事全部 mut 错误理解:先全 mut 过编译再说。
正确模型:只对真正变化的状态开 mut。
为何易错:从全可变语言迁来时的肌肉记忆。
与相邻概念对比
| 概念 | 关键问题 |
|---|---|
| 可变性 | 绑定是否允许赋值/可变操作 |
| 所有权 | 值由谁拥有、是否已 move |
| 借用 | 他人能否只读/独占写 |
三者正交但协作:先分清绑定,再学值怎么走。
工程实践
- 默认
let;编译器提示需要可变时再加mut。 - 解析与规范化管道优先 shadowing,避免
mut String里混太多阶段语义。 - 配置真相用
const或配置加载结果的不可变结构。
可验证实验
- 写
let x = 1; x = 2;观察编译错误文案。 - 写
let mut x = 1; x = 2;确认通过。 - 写
let x = 1; let x = "a";确认 shadowing 可换类型。 - 在内层 shadowing 后打印外层,确认作用域恢复。
本节总结
- 默认不可变;
mut显式打开写入。 - shadowing = 新绑定,可换类型。
const是编译期常量,区别于运行时不可变绑定。- 这是所有权与借用的前置语法心智。
自测题
概念题
- 为什么 Rust 默认不可变?
代码推理题
- 下列代码输出什么?
let s = "hi";
let s = s.len();
println!("{s}");
工程思考题
- 何时用 mut,何时用 shadowing?
参考答案
- 减少可变状态面,使推理、并发与借用检查更简单。
- 打印
2(hi的长度);类型从&str变为usize。 - 同一类型的状态更新用 mut;阶段性变换/换类型/收窄语义用 shadowing。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| The Book — Variables and Mutability | 官方书 | mut 与 shadowing |
| Rust By Example — Variable Bindings | 官方 | 绑定示例 |