如何访问可变成员而不破坏借用规则?
解读
在国内一线互联网/芯片/自动驾驶公司的 Rust 面试中,这道题常被用来区分“写过几段 Rust 代码”与“真正理解借用检查器”。
面试官真正想听的是:
- 你能否在不产生可变借用重叠的前提下,安全地拿到多个可变成员的引用;
- 你能否在编译期就证明这些引用不会指向同一块内存;
- 你能否在性能零损耗的前提下完成上述操作,而不是退回到运行时检查或克隆。
如果候选人直接回答“用 RefCell”或者“拆成多个结构体”,通常会被追问“还有更零成本的做法吗?”——这就引出了**“拆分借用(splitting borrow)”**这一核心思路。
知识点
- 独占可变引用(&mut T)的语义:同一作用域内只能存在一个,且不能与任何不可变引用共存。
- Rust 的“字段级借用精度”:编译器可以区分结构体内部不同字段,允许对多个字段同时建立互不重叠的可变引用。
- 拆分借用(Splitting Borrow):
- 手动解构:let Struct { a, b } = &mut self;
- 自建 getter 返回 &mut 不同字段;
- 标准库工具:slice::split_at_mut、iter_mut、Option::as_mut、RefCell::borrow_mut(运行时检查,非零成本)。
- unsafe 的替代方案:
- 使用 raw pointer 自行保证无重叠,但面试中必须给出充分不变量说明与单元测试方案,否则会被判“为炫技而牺牲安全”。
- 模式匹配与 NLL(Non-Lexical Lifetime):Rust 2018 以后,NLL 让字段级借用的生命周期精确到最后一次使用,大幅降低人工拆分的心智负担。
- 常见反模式:
- 同时调用 &mut self 的多个方法,导致二次可变借用;
- 把 &mut self 传入闭包后又去访问其他字段;
- 在迭代器里“偷偷”缓存 &mut 导致别名。
答案
“零成本且不破坏借用规则”的标准做法只有一句话:利用 Rust 的字段级借用精度,手动把 &mut self 拆成多个不重叠的 &mut 字段。
示例代码(面试现场可直接手写):
struct Cache {
map: std::collections::HashMap<u32, String>,
hits: u64,
misses: u64,
}
impl Cache {
// 需求:同时修改 map 和 hits,但编译器不允许两个 &mut self 共存
fn get_or_insert(&mut self, k: u32) -> &str {
// 1. 先拆分借用
let Self { map, hits, misses } = self;
// 2. 各自使用互不影响的可变引用
match map.get(&k) {
Some(v) => {
*hits += 1;
v
}
None => {
*misses += 1;
let v = map.entry(k).or_insert_with(|| k.to_string());
v
}
}
}
}
关键点讲解(面试时主动说出来):
- 解构赋值把 &mut self 一次性拆成三个互不重叠的 &mut 字段,生命周期由 NLL 精确控制;
- 没有运行时开销,也没有 RefCell 的计数成本;
- 如果字段间有逻辑耦合,可以封装成私有 helper,返回 (&mut A, &mut B) 元组,对外仍保持零成本抽象。
若面试官追问“字段数量多、层级深怎么办?”——可补充:
- 用 slice::split_at_mut 把大数组切成多段,每段给不同线程/任务;
- 用 iter_mut 遍历 Vec,编译器自动保证每次迭代拿到的 &mut 不重叠;
- 对递归结构体,可以写 unsafe 的 split_mut 方法,内部用裸指针+偏移量,但必须在文档里给出不变量:同一节点不会两次出现,并配套 miri 测试与单元测试,确保评审放心。
拓展思考
- 自引用结构体的困境:当字段 A 持有指向字段 B 的指针时,即使拆分借用也无法通过编译,此时需要 Pin + 投影(projection) 或者 unsafe 的 ouroboros 库,面试中可展示你对 Pin 语义的理解。
- 并发场景:想把两个字段同时丢给不同线程,可用 crossbeam::scope + split_at_mut,或者 ** rayon 的 par_iter_mut**,底层仍是零成本拆分借用。
- API 设计启示:对外暴露的方法如果返回 &mut self,会强制调用者串行化;更好的做法是返回零大小标记类型(如 Cursor)或拆分句柄,让调用者继续并行访问不同子字段,这在数据库内核、图形渲染管线中尤为常见。
- 与 C/C++ 对比:传统做法靠“指针 + 手动约定”实现字段级并发,Rust 在编译期就能给出等价证明,这正是国内安全关键行业(车载 OS、航天嵌入式)愿意用 Rust 重写底层模块的核心原因。