AI Library · 知识库

# Rust 专题:所有权、借用与生命周期

学习目标:深入理解 Rust 无垃圾回收保证内存安全的三大支柱:移动语义(Move)、借用规则(Borrowing)与静态生命周期注解(Lifetimes)。

前置知识与范围:Rust 基础语法与结构体。本篇通过真实代码与编译错误推导所有权在栈与堆内存上的管理细节。

# 所有权与移动语义

在很多带有垃圾回收(GC)的语言中,内存由运行时在后台追踪标记;而在 C/C++ 中,内存依赖程序员手工 malloc/free 或 new/delete。Rust 采取了编译期确定的第三种路线:所有权系统(Ownership)。

# 三条核心定律

  1. Rust 中的每一个值都有一个被称为其**所有者(Owner)**的变量。
  2. 同一时间内,一个值只能拥有一个所有者。
  3. 当所有者变量离开其所在的作用域(Scope)时,该值所占用的资源会被自动立即释放(Drop)。

# 移动(Move)而非浅拷贝

Rustfn main() {
    let s1 = String::from("hello");
    // 所有权发生移动:s1 将堆指针、长度与容量所有权转移给 s2
    let s2 = s1;

    // 此时 s1 已经失效,若尝试访问 s1 会在编译期直接报错
    // println!("{s1}"); // 编译错误:borrow of moved value: `s1`

    assert_eq!(s2, "hello");
    println!("s2: {s2}");
} // s2 离开作用域,"hello" 占用的堆内存在此自动 drop

在 Rust 中,像 String 这样管理堆资源的类型默认执行移动。这杜绝了 C/C++ 中两个指针指向同一堆地址并在各自离开作用域时发生的**双重释放(Double Free)**致命崩溃。

# 借用检查与别名排他律

如果每次传参都要转移所有权,代码会变得极其繁琐。Rust 引入了借用(Borrowing),即创建指向数据的引用而不获取其所有权。

# 借用的黄金法则

在任意给定的时间点,代码必须且只能满足以下两者之一:

演示修改与借用分离:

Rustfn append_suffix(s: &mut String) {
    s.push_str(" world");
}

fn calculate_len(s: &String) -> usize {
    s.len()
}

fn main() {
    let mut text = String::from("hello");

    // 不可变借用(读)
    let len = calculate_len(&text);
    assert_eq!(len, 5);

    // 可变借用(写)
    append_suffix(&mut text);
    assert_eq!(text, "hello world");

    println!("text: {text}");
}

# 为什么可变引用必须排他?

考虑数据竞争(Data Race)与迭代器失效(Iterator Invalidation): 在很多语言中,当你一边遍历列表(持有只读引用),一边向列表中追加元素(触发扩容搬移),原有的迭代器指针会瞬间指向野内存。在 Rust 中,这种代码在编译时就会被编译器拦截:因为 iter() 持有了不可变借用,此时无法同时调用 push()(需要可变借用)。

# 生命周期注解的本质

生命周期(Lifetime)并不是让程序员去延长某个对象的寿命,而是向编译器描述多个引用之间的存活期关系。

# 经典案例:返回较长切片

若一个函数接收两个字符串切片,并返回其中较长的一个:

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 string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        assert_eq!(result, "long string is long");
        println!("longest: {result}");
    }
    // 若在 string2 释放后依然尝试使用 result,rustc 立即拒绝编译!
}

若没有 'a 注解,编译器无法静态判断返回的引用究竟借用自 x 还是 y,因而无法在调用方验证返回的切片是否会发生悬垂引用。

# 结构体中的生命周期

当结构体需要存储一个借用引用时,结构体定义本身必须携带生命周期参数:

Ruststruct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt { part: first_sentence };

    assert_eq!(excerpt.part, "Call me Ishmael");
    println!("excerpt: {}", excerpt.part);
}

该标注保证了:Excerpt 实例的生命周期绝不能超过其引用的底本 novel 的作用域。

# 编译期错误复现与推导

为了加深理解,我们对比两个在 C/C++ 中极难定位但在 Rust 中会被编译器当场抓获的经典错误:

# 错误 1:悬垂引用(Dangling Reference)

在 C 中返回局部栈变量的指针非常容易导致未定义读取。在 Rust 中:

Rust// 试图返回对函数局部变量的引用
fn create_dangle() -> &String {
    let s = String::from("dangle");
    &s // 编译错误!`s` 在函数退出时就被释放了,返回引用将指向无效栈帧
}

rustc 报错明确指出:returns a reference to data owned by the current function,迫使你必须直接返回所有权(String)。

# 错误 2:读写借用冲突

Rustlet mut list = vec![1, 2, 3];
let first = &list[0]; // 不可变借用发生
list.push(4);         // 可变借用发生(可能导致 vector 重新分配堆内存)
println!("{first}");  // 不可变借用在此处仍被使用!

rustc 报错:cannot borrow list as mutable because it is also borrowed as immutable。这一条规则在源头彻底杜绝了并发内存撕裂与迭代器悬挂。

# 运行与验证

语言版本基线见示例基线。 保存上方完整程序为 main.rs,在终端执行 rustc --edition=2021 main.rs 并运行 ./main(Windows 下执行 .\main.exe):

纯文本s2: hello
text: hello world
longest: long string is long
excerpt: Call me Ishmael

测试断言已验证所有权转移、可变借用修改与生命周期绑定的合法执行。

# 相关阅读