Skip to content

所有权、借用与生命周期:无 GC 的内存安全基石

基于进阶语言 · 核于 2026-08

速查

  • 所有权三规则:①每个值有唯一所有者;②所有者离开作用域,值被自动释放Drop trait,RAII);③赋值/传参默认移动(move)——所有权转交,原变量失效;实现 Copy trait 的类型(i32/bool/&T)则栈上拷贝,仍可用。
  • move 的本质String/Vec/Box 这类持有堆内存的类型,move 只拷贝栈上的「指针/长度/容量」三元组,堆内存归属转移——既避免深拷贝开销,又杜绝双重释放。深拷贝要显式 clone()
  • 借用(引用):传 &T(不可变,可多个)或 &mut T(可变,唯一)。铁律:任意时刻「多个 &T」或「一个 &mut T」,二者不可共存——这是编译期消灭 aliasing bug 与数据竞争的根本。
  • 借用检查器(borrow checker):编译器组件,静态分析每个引用的活跃区间,拒绝违反规则的代码。一旦通过,运行时绝不可能出现悬垂引用/双重释放/数据竞争。
  • NLL(Non-Lexical Lifetimes):2018 版引入,引用生命周期到「最后一次使用」而非「作用域括号」,大幅减少无谓的编译错误。
  • 生命周期标注 'a:当函数有多个引用参数/返回引用时,用 'a 描述它们的关系(不改变实际寿命)。编译器据此验证「返回的引用不会比被引用者活得更久」。'static 表示活整个程序(字符串字面量)。
  • 悬垂引用 = 编译错误:函数不能返回局部变量的引用(局部变量函数结束即释放)。
  • 智能指针(堆上分配 + 额外语义)Box<T>(独占堆,单线程)、Rc<T>(引用计数,单线程共享)、Arc<T>(原子引用计数,多线程共享)、RefCell<T>/Mutex<T>(运行期借用检查/内部可变性)。String/Vec<T> 本质也是智能指针。
  • 所有权 vs GC vs 手动 free:Rust 所有权 = 编译期 + 确定性(无 STW);Java/Go GC = 运行期 + 非确定(有停顿);C/C++ 手动 = 运行期 + 确定但易错。

一、move 语义:默认是转移不是拷贝

C/C++ 默认赋值是拷贝(深拷贝昂贵,或浅拷贝致双重释放),Rust 反其道——默认 move:

rust
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;       // move:s1 的堆指针/长度/容量拷给 s2,s1 标记失效
    // println!("{}", s1); // ❌ value borrowed here after move

    let x = 5;
    let y = x;         // Copy:i32 实现了 Copy trait,栈上拷贝,x 仍可用
    println!("{} {}", x, y);
}
String 在内存中的布局(move 前):
  栈 s1:[ ptr ──┐, len=5, cap=5 ]

              ▼ 堆:[ h e l l o ]

move 之后:
  栈 s1:[ 失效 ]        栈 s2:[ ptr ──┐, len=5, cap=5 ]

                                        ▼ 堆:[ h e l l o ]   (同一块,归 s2)
  • 为什么安全:move 后 s1 在编译层面就失效了,再用它编译器直接报错——堆内存始终只有一个所有者,析构时不会双重释放。
  • Copy trait:栈上简单类型(i32/f64/bool/char/不含堆的元组/数组)实现 Copy,赋值时拷贝而非 move,原变量仍可用。要实现 Copy,类型必须所有字段都是 Copy(不能含 String/Box)。
  • 函数调用同理:传 String 给函数会 move(除非用引用),函数结束后该值在函数内释放。

二、借用与引用

每次都 move 太笨重(函数用完值就没了)。借用(borrow)让你不获取所有权地使用值:

rust
fn main() {
    let s = String::from("hello");
    let len = calculate_length(&s);   // 传 &s(不可变引用),s 所有权不变
    println!("'{}' 的长度是 {}", s, len);  // ✅ s 仍可用
}

fn calculate_length(s: &String) -> usize {  // 借用,不拥有
    s.len()
}   // s 离开作用域,但因为它不拥有,什么都不释放

两条借用规则(违反即编译错误):

  1. 多个不可变借用 OR 一个可变借用,不可同时存在:
    rust
    let mut s = String::from("hi");
    let r1 = &s; let r2 = &s;       // ✅ 多个 &
    let r3 = &mut s;                // ❌ 已有 &,不能再 &mut
  2. 引用必须始终有效(不可悬垂):
    rust
    fn dangle() -> &String {        // ❌ 返回局部变量的引用
        let s = String::from("hi");
        &s
    }   // s 在此释放,引用悬垂!
  • 可变借用要求被借变量 mutlet mut s,且只能有一个 &mut
  • 为什么这套规则消灭数据竞争:数据竞争 = 一写一读无同步。借用规则让「写」必须独占——单线程里消灭 aliasing bug,多线程里(配合 Send/Sync)编译期消灭数据竞争。

三、NLL:让借用规则更人性化

2015 版的借用规则按词法作用域判断,导致很多「看起来没问题」的代码报错。NLL(2018 版起默认)改为按最后一次使用点

rust
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);  // r1、r2 最后一次使用在此
// —— NLL 认为 r1/r2 在此之后不再活跃 ——
let r3 = &mut s;            // ✅ 可变借用(老规则会因 r1/r2 作用域到这而报错)
println!("{}", r3);

NLL 不改变安全保证,只是更精确地建模「引用何时真正结束」,让合理的代码能通过。

四、生命周期标注

当函数返回引用且参数有多个引用时,编译器无法自动推断返回值与哪个参数同寿命,需要你标注 'a

rust
// 'a 表示:返回的引用至少和 x、y 中较短的那个一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long");
    let result;
    {
        let s2 = String::from("xy");
        result = longest(s1.as_str(), s2.as_str());
        println!("{}", result);  // ✅ s2 还活着
    }
    // println!("{}", result);   // ❌ s2 已释放,result 可能指向 s2 → 编译错误
}
  • 'a 是描述而非命令:你声明「返回引用与参数同寿命」,编译器据此检查每次调用是否合规。
  • 结构体里的引用也要标注生命周期struct Foo<'a> { part: &'a str }——保证 Foo 不会比它借用的 part 活得久。
  • 生命周期省略规则:常见情况下编译器自动补全(每个引用参数一个生命周期、若只有一个输入则输出同寿命、&self/&mut self 方法输出同 self 寿命)。

五、智能指针:堆分配与多所有者

Box/Rc/Arc/RefCell 是「类引用但拥有数据」的智能指针,扩展所有权模型:

智能指针用途借用检查时机线程安全典型场景
Box<T>把值放到堆上,独占所有权编译期✅ Send递归类型(链表/树)、大对象避免栈拷贝
Rc<T>引用计数,多所有者(单线程)编译期❌ 非 Send共享只读数据(图、AST 节点互引)
Arc<T>原子引用计数,多所有者(多线程)编译期✅ Send + Sync多线程共享不可变数据
RefCell<T>内部可变性(运行期借用检查)运行期(panic 报错)单线程需要在不可变环境下修改(测试 mock)
Mutex<T>/RwLock<T>内部可变性 + 锁(多线程)运行期多线程共享可变数据
Cow<T>写时复制(借或拥有)编译期视 B 而定大部分只读、偶尔修改的场景
rust
use std::rc::Rc;

fn main() {
    // Box:堆上独占
    let b = Box::new(5);
    println!("{}", b);  // 自动解引用(Deref)

    // Rc:多所有者(单线程)
    let a = Rc::new(String::from("shared"));
    let b = Rc::clone(&a);  // 引用计数 +1,非深拷贝
    let c = Rc::clone(&a);
    println!("计数 = {}", Rc::strong_count(&a));  // 3
    // a/b/c 任何一个最后释放时计数减 1,归零才真正释放
}
  • Rc vs ArcRc 计数操作非原子(快但不线程安全),Arc 用原子操作(线程安全,多线程必用 Arc)。
  • Rc<T> 不能直接改:因为多个所有者,若可变会有数据竞争——要改就用 Rc<RefCell<T>>(单线程)或 Arc<Mutex<T>>(多线程)。
  • 循环引用内存泄漏Rc 互相引用会泄漏,需用 Weak<T> 弱引用打破环。

六、所有权机制对比其他内存管理

机制检查时机回收时机停顿代表
Rust 所有权编译期确定性(作用域结束)Rust
GC(追踪)运行期不确定(GC 触发)有 STWJava、Go、V8 JS
GC(引用计数)运行期计数归零无(但原子开销)Swift ARC、Python
手动 free手动C
RAII确定性(作用域结束)C++(析构)、Rust

Rust 的所有权是「编译期检查 + 确定性回收」的独特组合——既有 GC 语言的安全(且无停顿),又有 C/C++ 的性能与可控。

下一步

掌握了所有权/借用/生命周期后,下一步看Cargo、trait、泛型与生态——Rust 的工程化工具链、泛型单态化、trait object 动态分发,以及 Rust 在 WASM 与前端基建(SWC/Turbopack)中的崛起。