不可变借用的规则与限制: 从只读语义到零拷贝架构的 5 000 字深潜

0. 引言:为什么不可变借用是 Rust 并发的“第一公民”?
在 Rust 里,不可变借用 &T 同时承担了 两件事:
- 只读访问:禁止通过该引用修改目标数据。
- 共享并发:任意多个 &T可以同时存在,只要没有&mut T。
这条规则看似简单,却直接决定了:
- 能否 零拷贝 解析网络包
- 能否 并行迭代 rayon
- 能否 lock-free 读取全局配置
- 能否 安全地 暴露底层切片给 C
本文将:
- 从 词法 & 数据流 两个维度拆解不可变借用的 规则与限制
- 用 200+ 行代码 实现一个 零拷贝 JSON DOM + 并行查询 库
- 给出 常见误用清单 与 高级技巧(UnsafeCell、内部可变性、PhantomData)
1. 不可变借用的 3 条核心规则
| 维度 | 规则 | 违反后果 | 
|---|---|---|
| 别名规则 | 同一时刻可以存在 N 个 &T或 1 个&mut T,但不能混用 | E0502 | 
| 变异规则 | 通过 &T不能获得&mut T | E0596 | 
| 生命周期规则 | 不可变借用的 Region 必须 覆盖所有使用点 | E0597 | 
所有规则均在 编译期 由 borrow checker 在 MIR 层证明。
2. 编译器视角:Region + Loan + Access
2.1 MIR 数据流示例
fn demo(slice: &[i32]) -> i32 {let r1 = &slice[0]; // Loan₁ (shared)let r2 = &slice[1]; // Loan₂ (shared)*r1 + *r2
}
MIR 简化:
bb0:_2 = &(*_1)[0 of 2]; // Loan₁_3 = &(*_1)[1 of 2]; // Loan₂_4 = (*_2) + (*_3);
- Loan₁与- Loan₂均为 共享 → 合法
- Region 计算到 _4为止 → borrow checker 通过 ✅
2.2 违反示例:共享与可变混用
fn conflict(v: &mut Vec<i32>) {let r = &v[0];   // Loanₛ(v[0..4])v.push(4);       // Loanₘ(v[0..cap]) → 冲突 ❌println!("{}", r);
}
3. 不可变借用的 4 大限制
3.1 限制 1:不能升级成可变
fn cannot_upgrade(x: &i32) -> &mut i32 {&mut *x // ❌ E0596
}
3.2 限制 2:不能内部可变(默认)
use std::cell::Cell;
fn cell_demo(c: &Cell<i32>) {c.set(42); // ✅ Cell 内部可变性let r = &c.get(); // 不可变借用 Cell<i32>// *r = 99; // ❌ 不能修改内部值
}
不可变借用 默认 不可变,除非容器显式使用
UnsafeCell。
3.3 限制 3:不能跨越悬垂生命周期
fn dangling() -> &'static str {let s = String::from("hello");&s // ❌ E0515
}
3.4 限制 4:不能逃逸出所有者
struct Buffer<'a> {data: &'a [u8],
}impl<'a> Buffer<'a> {fn slice(&self) -> &'a [u8] {self.data // 合法:生命周期已连接}
}
4. 深度实战:零拷贝 JSON DOM + 并行查询
目标:
- 解析 JSON 字符串 → 不复制字节
- 构建 DOM → 所有字符串节点都是
&'input str- 并行遍历 →
rayon迭代器- 零 unsafe,100 % safe Rust
4.1 AST 设计
#[derive(Debug)]
pub enum Node<'a> {Null,Bool(bool),Num(&'a str),Str(&'a str),Arr(Vec<Node<'a>>),Obj(Vec<(&'a str, Node<'a>)>),
}
- 生命周期 'a与 输入字节切片 绑定
- 所有字符串节点都是 不可变借用,因此可以 任意共享 给多线程
4.2 解析器
use std::str::CharIndices;struct Parser<'a> {src: &'a str,chars: CharIndices<'a>,
}impl<'a> Parser<'a> {fn new(src: &'a str) -> Self {Self { src, chars: src.char_indices() }}fn parse(&mut self) -> Node<'a> {self.skip_ws();self.parse_value()}fn parse_value(&mut self) -> Node<'a> {match self.peek() {Some(b'n') => { self.consume("null"); Node::Null }Some(b't') => { self.consume("true"); Node::Bool(true) }Some(b'f') => { self.consume("false"); Node::Bool(false) }Some(b'"') => Node::Str(self.parse_str()),Some(b'[') => self.parse_arr(),Some(b'{') => self.parse_obj(),_ => Node::Num(self.parse_num()),}}fn parse_str(&mut self) -> &'a str {let start = self.chars.next().unwrap().0; // skip '"'while let Some((idx, ch)) = self.chars.next() {if ch == '"' {return &self.src[start + 1..idx];}}panic!("unterminated string");}fn parse_num(&mut self) -> &'a str {let start = self.chars.as_str().as_ptr() as usize - self.src.as_ptr() as usize;while let Some(ch) = self.peek() {if ch.is_ascii_digit() || ch == b'.' || ch == b'-' {self.chars.next();} else {break;}}let end = self.chars.as_str().as_ptr() as usize - self.src.as_ptr() as usize;&self.src[start..end]}fn parse_arr(&mut self) -> Node<'a> {self.chars.next(); // skip '['let mut out = Vec::new();self.skip_ws();if self.peek() == Some(b']') {self.chars.next();return Node::Arr(out);}loop {out.push(self.parse_value());self.skip_ws();match self.peek() {Some(b']') => { self.chars.next(); break; }Some(b',') => { self.chars.next(); self.skip_ws(); }_ => panic!("expected ',' or ']'"),}}Node::Arr(out)}fn parse_obj(&mut self) -> Node<'a> {self.chars.next(); // skip '{'let mut out = Vec::new();self.skip_ws();if self.peek() == Some(b'}') {self.chars.next();return Node::Obj(out);}loop {let key = self.parse_str();self.skip_ws();assert_eq!(self.chars.next().unwrap().1, ':');self.skip_ws();let val = self.parse_value();out.push((key, val));self.skip_ws();match self.peek() {Some(b'}') => { self.chars.next(); break; }Some(b',') => { self.chars.next(); self.skip_ws(); }_ => panic!("expected ',' or '}'"),}}Node::Obj(out)}fn skip_ws(&mut self) {while let Some(ch) = self.peek() {if ch.is_ascii_whitespace() {self.chars.next();} else {break;}}}fn peek(&mut self) -> Option<u8> {self.chars.clone().next().map(|(_, ch)| ch as u8)}fn consume(&mut self, s: &str) {for expected in s.bytes() {let actual = self.chars.next().unwrap().1 as u8;assert_eq!(actual, expected);}}
}
4.3 并行查询
use rayon::prelude::*;impl<'a> Node<'a> {fn find_strings(&self) -> Vec<&'a str> {match self {Node::Str(s) => vec![*s],Node::Arr(children) => children.par_iter().flat_map(|n| n.find_strings()).collect(),Node::Obj(entries) => entries.par_iter().flat_map(|(_, n)| n.find_strings()).collect(),_ => vec![],}}
}
- rayon的- par_iter需要- &'a str: Sync,而 不可变借用天生满足
- 无需任何锁,即可 并行遍历 100 MB JSON
5. 常见误用清单 & 修复
| 误用 | 现象 | 修复 | 
|---|---|---|
| 返回局部字符串 | &'a str指向String | 使用 Cow<'a, str> | 
| 误把 &T当&mut T | E0596 | 引入 Cell<T>或RefCell<T> | 
| 多线程写共享数据 | 数据竞争 | 使用 Arc<Mutex<T>> | 
| 跨 FFI 返回切片 | 悬垂 | 使用 借用守卫 RAII | 
6. 高级技巧:PhantomData 与零开销标记
当你需要 额外生命周期 但不占空间时:
use std::marker::PhantomData;struct Slice<'a> {ptr: *const u8,len: usize,_marker: PhantomData<&'a [u8]>,
}
- PhantomData参与借用检查,但 零大小
- 用于 FFI 或 自引用结构 的标记
7. 结语:不可变借用 = 并发友好的“只读契约”
- 编译期:borrow checker 用 Region 证明 只读 + 共享 的合法性
- 运行期:不可变借用天然 Sync,允许 零拷贝 + 并行
- 工程实践:善用生命周期、PhantomData、Cow,无需 unsafe 即可获得 C 级性能
愿你在下一次写解析器、网络库或并发框架时,把不可变借用当成 最锋利的工具!

