一句话总结
进入生命周期之前, 先把借用规则从"会用"升级到"会推理".
本篇深入借用检查器的思维方式: 它如何用 NLL 精确计算借用范围, 如何允许同时可变借用一个结构体的不同字段, 以及"重借用"为何能让 &mut 传来传去.
看懂这些, 生命周期就只剩最后一层窗户纸.
借用规则: 从规则到推理
基础篇记住了那条铁律--共享不可变, 可变不共享: 同一数据, 要么任意多个 &T, 要么唯一一个 &mut T, 二者不并存. 进阶篇要问的是: 编译器具体怎么判断两个借用是否冲突?
答案的核心是: 借用检查器关心的不是"写了几个 &", 而是"每个借用实际存活的范围是否重叠". 理解这一点, 很多"看似该报错却没报错"或"看似无害却报错"的现象就都说得通了.
把借用想象成图书馆的借阅记录: 规则不是"这本书总共被借过几次", 而是"在任意一个时间点, 是否存在冲突的借阅状态"--多人同时在馆内阅览(多个 &T)可以, 有人把书借走独占(&mut T)时就不能有别人.
借用检查器是那个只盯着"时间轴上有无冲突"的管理员.
NLL: 借用到"最后一次使用"为止
这是理解借用范围的关键机制, 叫非词法生命周期(NLL, Non-Lexical Lifetimes). 一个引用的存活范围, 不是从声明到作用域大括号结束, 而是从声明到它最后一次被使用为止. 之后它就"死了", 不再参与冲突判断.
|
如果按"借用持续到作用域结束"的旧规则, 上面会报错(可变与不可变借用共存). NLL 让编译器看得更细: 既然 r1/r2 在 r3 出现前就用完了, 自然无冲突.
实践指导: 尽早用完引用
NLL 给出一条避免借用冲突的实用策略: 让引用的使用尽量集中, 尽早结束.
很多"cannot borrow as mutable"报错, 只要把前面那个不可变借用的使用提前, 或用内层作用域 { } 框住, 就能解决--本质上是在帮编译器看清"这个借用其实已经不需要了".
可变性分割: 同时借用不同字段
一个常被忽略, 却极实用的能力: 借用检查器能识别"借用的是结构体的不同字段", 从而允许同时持有它们的可变引用.
|
因为 p.x 和 p.y 是内存上不相交的两块, 同时可变借用它们不会违反"可变不共享"--它们本就不是同一份数据.
但通过方法借用就不行
如果写的是 p.get_x_mut() 这样的方法, 编译器只能看到方法借用了整个 &mut self, 无法得知它内部只碰了 x. 于是同时调 get_x_mut() 和 get_y_mut() 会冲突.
这就是为什么有时需要直接访问字段, 或用标准库的 split_at_mut 等专门手段来"分割"借用. 这背后的取舍, 在写复杂数据结构时会反复遇到.
重借用: &mut 为什么能传来传去
&mut T 是不可 Copy 的(它要保证独占). 那为什么下面的代码能正常工作?
|
如果 &mut 像 String 那样被移动, 第一次 add_one(r) 后 r 就该失效了. 它没失效, 是因为发生了重借用(reborrow): 编译器自动把 add_one(r) 解释成 add_one(&mut *r)--临时从 r 再借一个新的可变引用传进去, 这个临时借用在函数返回时结束, r 的所有权从未离开.
重借用像"转借": 借来一本书(r), 把它转借给朋友看一会儿(add_one), 朋友看完还回来, 书又回到手上.
整个过程书的"主借阅人"始终是自己. 编译器自动安排了这场转借, 所以感觉不到.
为什么进阶篇要讲这些
NLL, 字段分割, 重借用, 平时多由编译器悄悄处理, 未必察觉. 但当代码变复杂, 报错变刁钻时, 正是这些机制在起作用.
理解它们, 才能从"为什么又报错了"的困惑, 走向"我知道编译器在想什么"的从容--这也是读懂下一篇生命周期标注的必要铺垫.
快速回顾
- 从规则到推理: 借用检查器判断的是"借用存活范围是否重叠", 而非
&的个数. - NLL: 借用到"最后一次使用"为止即结束; 尽早用完引用可化解多数冲突.
- 字段分割: 可同时可变借用结构体的不同字段(内存不相交); 但经由
&mut self的方法则不行. - 重借用:
&mut不可 Copy, 但传参时自动&mut *r临时转借, 原引用不失效. - 意义: 这些机制是读懂生命周期与复杂借用报错的地基.
动手练习
-
下面代码能编译吗? 如不能, 用 NLL 思路解释, 并修复.
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");
参考答案
不能编译. first 是对 v 的不可变借用, 且它在最后一行 println! 才最后一次使用--所以借用一直存活到那时. 而中间的 v.push(4) 需要 &mut v, 与仍存活的不可变借用冲突(且 push 可能触发扩容, 使 first 悬空, 这正是编译器要拦的).
修复: 把 first 的使用移到 push 之前, 或干脆不持有引用:
|
-
解释为什么下面能编译, 涉及哪个机制.
struct Pair { a: String, b: String }
let mut p = Pair { a: "x".into(), b: "y".into() };
let ra = &mut p.a;
let rb = &mut p.b;
ra.push('!'); rb.push('?');
参考答案
能编译, 靠的是可变性分割(disjoint borrows). p.a 和 p.b 是结构体中内存不相交的两个字段, 借用检查器能识别"它们不是同一份数据", 因此允许同时持有 &mut p.a 和 &mut p.b, 不违反"可变不共享".
但如果改成通过 &mut self 的方法分别取 a, b 的可变引用, 就会冲突, 因为编译器只看到整个 self 被借走.
- 写一个函数
fn bump(n: &mut i32)把值加一, 然后在main中对同一个&mut连续调用它两次, 解释为什么第二次调用时引用没有失效.
参考答案
|
因为发生了重借用: bump(r) 被编译器解释为 bump(&mut *r), 每次只是从 r 临时借出一个新的可变引用, 函数返回时该临时借用结束, r 的所有权从未被移走, 所以可以反复使用.