- 文档
- 教程
【免费下载链接】rust-by-example
Learn Rust with examples (Live code editor included)
Rust 中的“冻结”是指当数据被同名的不可变绑定遮蔽(shadowing)后,该数据在当前作用域内暂时失去可修改性,直到这个不可变绑定离开作用域才恢复。本篇指南以 src/variable_bindings/freeze.md 为核心,结合 Rust By Example 仓库中 Mutability、Scope and Shadowing 等章节,完整讲解冻结的触发条件、作用域边界、与遮蔽和可变性的关系,并给出可直接运行的示例与排查思路。
什么是变量冻结(Freezing)
在 Rust 中,变量绑定默认不可变,可以通过mut关键字显式声明可变绑定(详见 Mutability)。冻结(Freezing)是一个由绑定组合引出的特殊状态:
当数据被同一个名字以不可变方式绑定时,该数据会被“冻结”(freezes)。被冻结的数据在不可变绑定离开作用域之前,无法被修改。
这句话包含三个关键要素:
- 触发方式:用不可变绑定(
let不加mut)对某个名字进行遮蔽(shadowing); - 冻结对象:被该名字指向的数据(即使它底层原本是可变的);
- 解除条件:不可变绑定所在的作用域结束(block 结束或函数结束)。
冻结并不改变底层数据的存储方式,也不产生拷贝或移动,它只是编译器在某个作用域内对“这个名称不可被赋值”这一约束的体现。
核心示例逐行剖析
freeze.md提供的完整示例位于仓库 src/variable_bindings/freeze.md,它同时带有editable、ignore、mdbook-runnable三个代码属性——前者表示 mdBook 渲染后可在线编辑运行,ignore表示不会参与mdbook test的编译测试,因为示例本身刻意包含编译错误。
fn main() { let mut _mutable_integer = 7i32; { // Shadowing by immutable `_mutable_integer` let _mutable_integer = _mutable_integer; // Error! `_mutable_integer` is frozen in this scope _mutable_integer = 50; // FIXME ^ Comment out this line // `_mutable_integer` goes out of scope } // Ok! `_mutable_integer` is not frozen in this scope _mutable_integer = 3; }逐行解读:
- 第 1 行:
let mut _mutable_integer = 7i32;声明了一个可变绑定,类型为i32,初值7。名字前的下划线用于抑制“未使用变量”警告(与 Variable Bindings 章节中的_unused_variable用法一致)。 - 第 4~5 行:进入一个新的代码块
{ }。块(block)是 Rust 中界定作用域的基本单位,块内绑定只在该块内存活,这正是 Scope and Shadowing 强调的规则。 - 第 6 行:
let _mutable_integer = _mutable_integer;是关键一步。右侧的_mutable_integer读取外部可变绑定的值(i32是Copy类型,这里是拷贝),随后左侧用同名的新绑定遮蔽旧绑定。由于新绑定没有mut,此刻起该名字指向的数据被视为冻结。 - 第 8~9 行:
_mutable_integer = 50;尝试对冻结的数据赋值,编译器报错cannot assign to_mutable_integer, as it is not declared as mutable。示例中的FIXME注释提示读者:把这一行注释掉即可消除错误。 - 第 12 行:块结束,内部不可变绑定销毁并离开作用域,冻结随之解除。
- 第 15 行:回到外层作用域后,
_mutable_integer = 3;重新赋值成功——因为它依然是那个mut可变绑定,从未被真正改变过可变性。
运行方式:由于示例刻意包含编译错误,请先注释掉第 9 行的赋值语句,再使用rustc freeze.rs && ./freeze编译运行;也可以打开仓库 book.toml 中配置的 mdBook 环境(edition = "2021",且[output.html.playpen]开启editable = true),直接在浏览器里在线编辑体验。
冻结与遮蔽(Shadowing)的配合机制
冻结不是独立的语言特性,而是“可变绑定 + 同名不可变遮蔽”叠加的结果,其前提正是 Rust 允许变量遮蔽(variable shadowing)。
从 Scope and Shadowing 可知,内层块可以用let声明与外部同名的绑定,内层绑定会“遮蔽”外层绑定,块结束后外层绑定重新可见:
fn main() { let shadowed_binding = 1; { println!("before being shadowed: {}", shadowed_binding); // This binding *shadows* the outer one let shadowed_binding = "abc"; println!("shadowed in inner block: {}", shadowed_binding); } println!("outside inner block: {}", shadowed_binding); // This binding *shadows* the previous binding let shadowed_binding = 2; println!("shadowed in outer block: {}", shadowed_binding); }冻结与普通遮蔽的差异在于遮蔽绑定的可变性:
| 遮蔽方式 | 行为 |
|---|---|
| 不可变绑定遮蔽不可变绑定 | 新绑定仍然不可变,正常遮蔽,不可赋值 |
| 不可变绑定遮蔽可变绑定 | 被遮蔽的可变数据进入冻结状态,不可赋值 |
可变绑定(mut)遮蔽旧绑定 | 新绑定可变,可以赋值,不存在冻结 |
从源码结构看,这一行为与 Rust 编译器的名称解析(name resolution)和借用检查策略一致:作用域内同一名字只对应一个绑定,编译器只认可当前生效绑定的可变性。因此冻结本质上是名字级(binding-level)的临时只读化,而非对底层内存区域的持久限制。
冻结与可变性(Mutability)的关系
冻结概念只有对照 Mutability 才能完整理解。Rust 变量绑定默认不可变,mut是唯一的“可变性开关”:
fn main() { let _immutable_binding = 1; let mut mutable_binding = 1; println!("Before mutation: {}", mutable_binding); // Ok mutable_binding += 1; println!("After mutation: {}", mutable_binding); // Error! Cannot assign a new value to an immutable variable _immutable_binding += 1; }两者对比可以提炼出 Rust 可变性设计的完整梯度:
- 声明时定死:可变性在
let/let mut声明那一刻决定,且不可在同一作用域内改变; - 通过遮蔽改变可见性:要“临时”让一个可变数据变得不可变,唯一手段就是用不可变绑定遮蔽它——这正是冻结;
- 通过作用域解除:冻结随遮蔽绑定的作用域结束而自动解除,无需手动“解冻”操作;
- 编译期强制执行:对冻结数据赋值、对不可变绑定赋值,都会触发编译器详细的诊断信息(mutability error),错误信息会明确指出绑定未声明为
mut。
冻结的解除:作用域是唯一的钥匙
freeze.md示例的核心戏剧性就在于:同一个名字、同一个变量,内层块中被冻结,外层块中却可以正常赋值。这充分说明冻结的生命周期严格绑定于产生遮蔽的不可变绑定的作用域:
- 冻结开始:不可变遮蔽绑定生效的那一刻(进入块、执行
let); - 冻结持续:遮蔽绑定存活期间;
- 冻结结束:遮蔽绑定离开作用域(块结束)或函数返回。
这种按作用域生效的设计与 Rust 的 RAII 理念一脉相承——资源的释放、可变的恢复都不需要手动管理,而是由作用域自动驱动。同时它也和所有权(Ownership and moves)一起,构成了 Rust “编译期保证内存与可变性安全”的完整拼图。
实际应用与使用建议
冻结在实战中并不需要刻意制造,但理解它有助于排查两类典型问题:
- “为什么我在内层块里改不了外层变量?”:排查是否在内层块里用
let(而非let mut)对同名变量做了遮蔽。此时内层绑定可能是冻结状态,或者是一个全新的不可变绑定。区分方法很简单:在外层块里是否还能赋值——能,说明只是内层遮蔽;不能,说明内外层共享同一个不可变绑定。 - “把可变变量传给函数后还能用吗?”:这不是冻结,而是所有权移动(move),见 Ownership and moves 中的
Box<i32>示例。冻结与移动是两种不同的机制:冻结不转移所有权,只是暂时禁止赋值。
最佳实践建议:平时应避免用同名遮蔽来“制造冻结”,因为这会降低代码可读性(正如 Declare first 所述,声明与使用分离会让读者更难定位初始化位置)。推荐的做法是:要么从一开始就声明为不可变绑定,要么让需要修改的变量保持mut并在不同作用域使用不同命名。冻结概念真正的价值在于帮助你读懂编译器报错——当编译器提示“该绑定不可变/被冻结”时,你能立刻定位到是哪一层遮蔽在起作用。
相关章节导航
围绕冻结主题,仓库 src/SUMMARY.md 按如下顺序编排了完整的学习路径,建议按序阅读:
- Variable Bindings:
let绑定与类型推断基础; - Mutability:
mut修饰符与默认不可变原则; - Scope and Shadowing:块作用域与遮蔽规则,理解冻结的前提;
- Declare first:先声明后初始化的边界约束;
- Freezing(本篇):冻结机制本身。
此外,Ownership and moves 与 RAII 可帮助你把冻结放入 Rust 更宏大的安全模型中理解:可变性、所有权与资源生命周期全部由编译器在编译期静态保证,无需运行时开销——这正是 Rust By Example 全书的核心理念。
- 文档
- 教程
【免费下载链接】rust-by-example
Learn Rust with examples (Live code editor included)
相关推荐
rust-by-example 深度解析:Rust 可变借用(&mut T)的规则、限制与实战
rust by example 深度解析:Rust 可变借用(&mut T)的规则、限制与实战 导读 可变借用是 Rust 所有权与借用体系中最重要的机制之一:
文档教程Rust 变量绑定(Variable Bindings)深度指南:let、不可变性与遮蔽机制
Rust 变量绑定(Variable Bindings)深度指南:let、不可变性与遮蔽机制 导读 :变量绑定是 Rust 一切程序的基石—— let foo
教程文档rust-by-example:深入解析 `macro_rules!` 宏重载(Overload)机制
rust by example:深入解析 macro_rules! 宏重载(Overload)机制 Rust 的 macro_rules! 允许同一宏名对应多组
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考