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 是编译期常量,不是运行时可绑可变存储。

最小可运行示例

场景:订单服务处理一次优惠券改写与审计字段升级

结账流水线里有三段状态:

  1. 商品件数:处理过程中会改(需要 mut
  2. 客户显示名:从原始字符串“净化”成新字符串(常用 shadowing,甚至类型从 &strString
  3. 服务名常量:全进程编译期固定(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

结合场景再看三个关注点

  1. 可变性是绑定的属性mut 说的是“能不能通过这个名字写”,与“值是否 Copy”是不同轴。
  2. shadowing 适合变换流水线trim → lowercase → owned,每一步一个更精确的绑定。
  3. 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. conststatic(入门边界)

  • 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
借用他人能否只读/独占写

三者正交但协作:先分清绑定,再学值怎么走。

工程实践

  1. 默认 let;编译器提示需要可变时再加 mut
  2. 解析与规范化管道优先 shadowing,避免 mut String 里混太多阶段语义。
  3. 配置真相用 const 或配置加载结果的不可变结构。

可验证实验

  1. let x = 1; x = 2; 观察编译错误文案。
  2. let mut x = 1; x = 2; 确认通过。
  3. let x = 1; let x = "a"; 确认 shadowing 可换类型。
  4. 在内层 shadowing 后打印外层,确认作用域恢复。

本节总结

  • 默认不可变;mut 显式打开写入。
  • shadowing = 新绑定,可换类型。
  • const 是编译期常量,区别于运行时不可变绑定。
  • 这是所有权与借用的前置语法心智。

自测题

概念题

  1. 为什么 Rust 默认不可变?

代码推理题

  1. 下列代码输出什么?
let s = "hi";
let s = s.len();
println!("{s}");

工程思考题

  1. 何时用 mut,何时用 shadowing?
参考答案
  1. 减少可变状态面,使推理、并发与借用检查更简单。
  2. 打印 2hi 的长度);类型从 &str 变为 usize
  3. 同一类型的状态更新用 mut;阶段性变换/换类型/收窄语义用 shadowing。

延伸阅读与资料来源

资料类型支撑内容
The Book — Variables and Mutability官方书mut 与 shadowing
Rust By Example — Variable Bindings官方绑定示例
创建于 2026/7/15 更新于 2026/7/15