Rust 所有权深入:借用、生命周期,以及那个真正帮你的编译器
Rust 的第一个月是和 borrow checker 打架。第二个月你开始发现它在抓真 bug。第三个月你内化了模型,编译器变成协作者。这是我希望第一个月就看到的指南。
三条规则,重新理解
多数教程列出来就完事。我想多停留一下。
- 每个值有且只有一个所有者。
- 借用可以共享(多个
&T)或独占(一个&mut T),但不能同时存在。 - 借用不能比所有者活得长。
注意缺什么:没有 GC、没有引用计数(默认)、没有手动 free()。所有者作用域决定值何时 drop。其他规则都是这条的推论。
move、copy,以及为什么 String 和 i32 行为不同
let s = String::from("hello"); let s2 = s; // println!("{}", s); // 错误:s 已被 move
对比:
let n = 42; let n2 = n; println!("{} {}", n, n2); // 都能用
区别:String 拥有堆内存,没实现 Copy。赋给 s2 转移所有权,s 失效。i32 是 Copy(小、不在堆上),赋值复制位。
第一反应是"到处加 .clone()"。别这样。对的反应是改成借用。
借用:日常货币
fn greet(name: &str) { // 借用 println!("Hello, {}", name); } let s = String::from("world"); greet(&s); // 传借用 println!("{}", s); // s 仍可用
&str 是字符串的借用视图。greet 读它但不接管。返回后 s 仍是它堆数据的所有者。
容易踩的规则:借用活着时,不能改原值,也不能创建独占借用。
let mut v = vec![1, 2, 3]; let first = &v[0]; v.push(4); // 错误 println!("{}", first);
如果 v.push 触发扩容,first 就是悬空指针。编译器拒绝。这是 borrow checker 配工资的时候。
共享 vs 独占:防止数据竞争的规则
fn append(buf: &mut String, s: &str) { buf.push_str(s); } let mut buf = String::new(); append(&mut buf, "hi"); let a = &buf; let b = &mut buf; // 错误:有共享借用时不能创建独占借用
&mut T 是独占的 —— 它存在时,没有其他引用(共享或独占)能存在。这就是 Vec、HashMap 等可以不加锁地安全修改的原因:类型系统已经证明没人在读。
口号:共享 XOR 可变。同一洞见驱动整个 Sync / Send 和并发故事。
生命周期:没那么吓人
生命周期是编译器追踪"引用必须保持有效多久"的方式。多数时候 你不用写,因为有生命周期省略。
三条省略规则:
- 每个输入引用拿到自己的生命周期。
- 只有一个输入生命周期时,赋给所有输出。
- 有
&self/&mut self时,把 self 的生命周期赋给所有输出。
这些规则覆盖 ~95% 函数签名。剩下 5% 显式写:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } }
'a 说"返回的引用至少和两个输入一样长寿"。不写编译器不知道继承哪个。
当 borrow checker 跟你打架,听它的
borrow checker 的"误报"几乎总是真设计问题。
- "cannot borrow as mutable because already borrowed as immutable." 真问题:抓着不可变引用还想改容器。要么先丢借用,要么先复制出需要的部分。
- "borrowed value does not live long enough." 真问题:返回了局部变量的引用。要么返回所有权,要么把引用当参数收。
- "use of moved value." 真问题:值已交出去还在用。改成借用,或重构。
真的需要共享可变状态?用显式工具:单线程 Rc<RefCell<T>>,多线程 Arc<Mutex<T>>。这些把检查从编译期挪到运行期 —— 它们是逃生口,不是默认。
编译过的常见模式
返回 self 的借用:
struct Db { rows: Vec<String> } impl Db { fn first(&self) -> Option<&str> { self.rows.first().map(|s| s.as_str()) } }
返回的 &str 通过省略规则 3 继承 &self 生命周期。
owned self 的链式构建器:
impl Request { pub fn header(mut self, key: &str, val: &str) -> Self { self.headers.insert(key.into(), val.into()); self } }
owned self 允许链式 —— 每次调用把 self move 到下一次。
避免在 struct 字段写生命周期:
// 痛苦 struct View<'a> { data: &'a Data } // 简单 struct View { data: Arc<Data> }
Arc(或 Rc)用一点点运行期开销换"不用把生命周期参数透过每个用 View 的函数"。借用真和作用域绑定时用生命周期,共享所有权用 Arc。
心智的转变
其他语言让你赖账:"先抓着引用,GC 之后清"或"先加锁,运行时调度"。Rust 强迫你提前决定:谁拥有?谁借?多久?独占还是共享?
那个决定就是数据结构设计。一旦你把所有权当成一等的 API 选择(和类型与函数签名并列),borrow checker 就不再是障碍。它就是类型检查你本来想写的那个设计。
TL;DR
- 每个值一个所有者;赋值时 move,除非类型是
Copy。 - 借用要么共享(
&T)要么独占(&mut T),不能同时。 - 生命周期多数被省略;只有编译器分不清输出借用谁时才写。
- borrow checker 错误通常指向真设计问题 —— 听它的。
- 真需要共享可变状态用
Rc<RefCell<T>>或Arc<Mutex<T>>显式声明。
第一个月编译器是敌人,第三个月它是抓 linter 抓不到的 bug 的同事。相信过程。
相关阅读
TypeScript 类型体操:什么时候值,什么时候在炫技
务实的 TypeScript 高级类型指南 — mapped types、conditional types、template literal types 真正能给你什么,什么时候用,什么时候应该退回到朴素代码。
Kubernetes 资源 requests / limits 实战:不会把生产搞挂的设法
怎么在生产里实际设 Kubernetes CPU 与内存的 requests/limits — QoS 类、CPU 节流、OOM kill、那些害公司钱的差别,以及好使的模式。
Vue 3 vs React 2026:下个项目的诚实对比
2026 年 Vue 3 与 React 的诚实对比 — Composition API vs Hooks、性能、生态、TypeScript 表现,以及真正决定选型的标准。