本节摘要:解释 let 默认绑定不可变值的原因,演示 E0384 判例;区分"遮蔽"与"可变"两种改变值的途径,并说明 const 与 let 的分工。读完你能在"改一个值"时选对合法路径。
let verdict = "无罪"; verdict = "有罪";
判词:cannot assign twice to immutable variable(E0384)。Rust 里 let 声明的变量默认冻结,想改须在声明处申明 mut:
let mut verdict = "无罪"; verdict = "有罪"; // 合法:事先取得可变许可
为什么默认反向?不可变引用可以到处分发(第 3 章的共享旁听权),只有证明"只有我一人在用"时才授予写权限。默认不可变让这个证明在大多数代码里自动成立——这是并发安全的制度基础,不是刁难。
let id = "A-173"; let id = id.len(); // 新的 id,类型都变了,usize
遮蔽(shadowing)声明了一个新变量,旧值到期。它和 mut 的区别在司法意义上完全不同:
| 途径 | 语义 | 能否改类型 | 所有权影响 |
|---|---|---|---|
| mut | 同一变量的写许可 | 不能 | 就地修改,通常更省分配 |
| 遮蔽 | 新变量顶替旧名 | 能 | 新绑定,旧值按作用域释放 |
字符串处理的惯用链就是遮蔽串起来的:解析、修剪、转小写,每步类型在变,名字不变。
const MAX_CASES: u32 = 500; 编译期内联、必须标类型、命名全大写。常量不是变量,没有"可变版本",也不占运行时内存位置。全局可变状态(static mut)则几乎总被并发法庭驳回,第 8 章再审。
⚠️ 常见坑:把遮蔽当 mut 用来"改"循环计数,结果每轮都新建变量,旧值根本没动。要就地修改,写 mut。
初学者常把 shadowing(遮蔽)当成 mut 的另一种写法,两者其实是完全不同的判决。遮蔽是"新建一个同名变量顶替旧值",旧值原地不动直到离开作用域;mut 是"同一块产权上允许原地修改"。最直观的差异是类型可以借遮蔽更换。
fn parse_id(raw: &str) -> u32 { let raw = raw.trim(); // 遮蔽一:&str 换成去掉空白的新 &str let raw: u32 = raw.parse() // 遮蔽二:直接换成 u32 .expect("编号应为数字"); raw // 此处的 raw 已是 u32 } fn accumulate(v: &mut Vec<i32>) { v.push(1); // mut:同一 Vec 原地追加 v.push(2); }
遮蔽的工程价值在"转换管线":每一步都以同一个名字承载当前形态,读者不需要记 total_str、total_num 这类累赘命名。但注意遮蔽不改变所有权规则——若被遮蔽的是持有堆内存的值,旧值继续存活到作用域结束,这一点可用 drop 验证。
不可变不止 let 一条路,但另两条路的法律地位不同,混用会埋下隐患。
| 声明 | 求值时机 | 内存身份 | 典型用途 |
|---|---|---|---|
const MAX: u32 = 100; |
编译期,逐处内联 | 无独立地址 | 纯常量、数组长度 |
static LABEL: &str = "案"; |
编译期,唯一地址 | 固定内存位置 | 需要稳定地址、全局引用 |
let x = 100; |
运行期 | 栈上产权 | 一切默认起点 |
const 没有固定地址,取其引用每次可能指向不同副本;static 有唯一地址,且 'static 生命周期由语言保证——这使它成为第 3 章生命周期一节的常客。带 static mut 的写法是不安全区的内容,第 10 章再开庭审。
不可变判决派生出两条高频 lint。unused_mut 提示声明了 mut 却从未修改——多半是重构残留,删掉 mut 即可;unused_variable 提示变量从未读取——若确属占位,按提示改名 _ 前缀显式声明"暂不采信"。这两条不是噪音,它们在替你做一次免费的代码评审:可变性授出去却没用,本身就是判决书上的污点,因为每一分 mut 都会缩小借用检查器的回旋余地。
下面这段代码把本章三个考点装在一起,先自己判,再看注释对答案。
fn main() { const LIMIT: u32 = 100; static COURT: &str = "第一法庭"; let mut used = 0u32; // 声明可变 used += 30; let used = used as u8; // 遮蔽:换成 u8,旧 used 就此冻结 // used += 10; // 驳回:新 used 未声明 mut,E0596 let used: u32 = used as u32 + LIMIT; println!("{} {} {}", COURT, used, LIMIT); // 输出:第一法庭 130 100 }
三个考点分别是:const 与 static 各就其位;遮蔽切换类型后可变性归零;遮蔽链上的每一步都是新产权。能在不运行的情况下说对输出的,本章即可结案。
写 review 时对 mut 的三条问询:这个变量真的原地修改了吗(否则 E0184 提示去掉 mut);可变引用的存活区间能否再缩(配合第 3 章);结构体字段要不要逐字段 mut(Rust 支持字段级可变,借出 &mut self.field 比借整个 self 更精准)。第三条在大型结构体上价值明显——字段级借用让"改 A 字段同时读 B 字段"合法化,这是很多"看似要 clone"场景的真正免费解法。
SCREAMING_SNAKE_CASE 之外的约定也值得一次说清:const 用于真常量(物理系数、协议值),static 用于唯一实例(全局配置引用、惰性初始化的锚点),let 绑定是一切变量的默认席。混用的典型症状是"把运行期计算结果塞进 const"——const 要求编译期可求值,塞不进时新手常误改 static 并引入可变性。正确解法几乎总是:普通 let 加函数封装,需要全局访问再讨论 OnceLock 一类初始化设施。命名之外,看到 static mut 直接按第 10 章的不安全区处理,普通业务代码没有它的席位。
最后一个综合现场:统计一段文本里每个单词的出现次数,变量该怎么声明。答案骨架——计数表 let mut tally: HashMap<_,u32> 必须可变;遍历用的分割结果只需只读绑定;累计值在 entry 闭包内通过可变引用修改,外层变量本身无需 mut。这个例子说明 mut 的颗粒度判断已成为日常动作:每写一个 mut,都应能说出"谁在哪个区间修改它"。说不出的 mut,就是下一次借用冲突报错的预定现场。