所有权、借用与生命周期
📚 Rust 课程系列
所有权(ownership)、借用(borrowing)与生命周期(lifetime)是 Rust 最核心、也最与众不同的特性。它们让 Rust 在没有垃圾回收、也不需要手动 free 的前提下,于编译期保证内存安全与无数据竞争。本章先建立所有权的直觉,再讲清借用检查器的规则,最后引入生命周期注解——这是理解后续智能指针、并发、异步等章节的基础。
所有权与借用
所有权的基本规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 唯一所有者 | 每个值有且仅有一个所有者,离开作用域时自动释放 | let s = String::from("hi"); |
| 移动语义 | 赋值/传参时所有权转移,原变量失效 | let s2 = s1; —— s1 不再可用 |
| 复制语义 | Copy 类型赋值时按位复制,原变量仍有效 | let y = x;(x: i32) |
1 | |
💡 提示:为什么
String赋值后原变量失效?String在栈上存放指针/长度/容量,堆上存放实际数据。let s2 = s1;只复制了栈上部分——若两者同时有效,就会指向同一块堆内存,离开作用域时 double free。Rust 用移动语义在编译期杜绝此问题,零运行时开销。
🔄 对比:Rust 的移动语义 ≈ C++ 的
std::unique_ptr语义,但 Rust 编译器强制执行——你无法意外复制。Go/Java 的赋值总是复制引用(或值),不存在"移动"概念。
💡 提示:判断能否
Copy的简单规则——若类型销毁时需要特殊清理(如String要释放堆内存),就不能Copy;标量、不可变引用、全是Copy字段的结构体可以Copy。
函数与所有权
传参和返回同样遵循移动/复制语义,让「谁负责释放」始终清晰:
1 | |
💡 提示:如果函数只是需要「读一下」而不想拿走所有权,每次都先拿走再返回很笨重——这就是借用的意义。
借用
使用 & 创建不可变引用,&mut 创建可变引用。引用不拥有数据,只是「指向」某个值:
1 | |
要修改借用的值,用 &mut T,但原变量本身也得是 mut:
1 | |
🔄 对比:
&T≈ C++ 的const T&,&mut T≈T&。但 Rust 编译器额外保证:同一时间要么有多个&T,要么有且仅有一个&mut T,从源头杜绝数据竞争。
借用检查器规则
规则总结:
- 任何借用必须在其引用的值的生命周期内有效
- 可以同时存在多个不可变借用(
&T) - 不能同时存在可变借用(
&mut T)和任何其他借用
1 | |
💡 提示:上面
r1、r2在println!之后就「不再使用」了,因此r3 = &mut s可以通过。这说明 Rust 的借用并非严格按词法作用域结束,而是采用 非词法生命周期(NLL):引用的「活跃区间」只到它最后一次被使用为止。
⚠️ 注意:在持有不可变借用时试图修改数据是典型报错场景:
1 | |
原因:push 可能触发扩容并把堆数据搬到新地址,此时 first 指向的旧地址就悬空了。修复方式是把不可变借用的使用提前到可变操作之前:
1 | |
可变引用还有一个限制:同一时间只能有一个可变引用,从源头杜绝数据竞争:
1 | |
⚠️ 注意:遍历容器同时修改(如
for item in &vec { vec.push(...); })会被编译器拒绝——这是借用冲突最常见的场景之一。优先收集修改操作到遍历之后,或使用索引遍历。
💡 提示:函数参数设计原则——不需要修改用
&T,需要修改用&mut T,需要获得所有权(如构建返回值)传T。对大型结构频繁clone()有性能开销,优先考虑借用或Rc/Arc。
非词法生命周期(进阶)
NLL 自 Rust 2018 edition 起成为默认行为。在旧版词法生命周期(lexical lifetimes)下,引用的存活区间等于整个作用域——即使引用在作用域中途就不再使用,借用检查器也会认为它一直活跃到作用域末尾。NLL 将引用的存活区间缩短到最后一次使用点,使很多在词法模型下被拒的代码合法化:
1 | |
🔬 进阶:NLL 的实现基于「生命周期即约束」模型——编译器先收集所有约束(
'a: 'b表示'a至少和'b一样长),再求解最小可行生命周期,而非简单地将引用绑定到词法作用域。
生命周期
为什么需要生命周期
引用必须保证它指向的数据在引用使用期间一直有效,否则就是「悬垂引用」。大多数时候编译器能自动推断,但当函数返回引用时,编译器无法仅凭签名判断返回的引用和哪个参数有关联——这时就需要生命周期注解。
生命周期注解用 'a 表示,它不改变引用的实际存活时间,只是描述多个引用之间的约束关系:「返回的引用至少和入参 x、y 中较短的那个一样长」。
1 | |
'a 表示:返回值的生命周期等于 x 和 y 生命周期中较短的那个。调用方据此就知道返回值不能比 x、y 活得更久。
⚠️ 注意:不要返回对局部变量的引用——编译器会直接拒绝:
1 | |
正确做法是直接返回所有权:fn no_dangle() -> String { String::from("hello") }。
💡 提示:当生命周期签名复杂时,考虑返回拥有所有权的类型(如
String)或使用Arc/Cow来简化 API。在接口设计中尽量让调用方持有所有权或明确借用关系,减少不必要的生命周期注解暴露。
生命周期省略规则
Rust 编译器会自动应用三条规则来省略生命周期注解:
| 规则 | 说明 |
|---|---|
| 规则 1 | 每个引用参数都有自己的生命周期参数 |
| 规则 2 | 如果只有一个输入生命周期参数,它被赋予所有输出生命周期参数 |
| 规则 3 | 如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,则 self 的生命周期被赋予所有输出生命周期参数 |
1 | |
按规则 1,s: &str 被看作 s: &'a str;按规则 2,唯一的输入生命周期 'a 被赋给输出,于是签名等价于 fn first_word<'a>(s: &'a str) -> &'a str,无需手写。当三条规则都无法确定输出生命周期时,编译器才要求你显式标注(如 longest 有两个输入引用却没有 self,规则 3 不适用,故必须手写)。
结构体中的生命周期
结构体若持有引用,必须标注生命周期,表明该引用的数据必须比结构体活得久:
1 | |
🔄 对比:C++ 中类持有引用不需要显式标注生命周期,但可能产生悬垂引用(运行时 UB);Rust 在编译期就强制你声明约束关系。
静态生命周期
1 | |
'static 表示引用在程序整个运行期间都有效。所有字符串字面量都是 &'static str,因为它们直接被编译进二进制的只读数据段。'static 是所有生命周期的「上界」:'static 引用可以满足任意 'a 的约束,因此常出现在需要「任意生命周期」的地方。
⚠️ 注意:不要为了消除编译错误而随手给参数标
'static,这往往掩盖了真正的设计问题(本该返回拥有所有权的String却返回了引用)。真正需要'static的场景包括:全局常量、std::thread::spawn闭包的默认约束等。
生命周期与型变(variance)、'static 约束的更深入讨论,见《类型系统》一章。


