Rust 入门(三):所有权、借用与生命周期

January, 9th 2026 13 min read Markdown
Rust 入门(三):所有权、借用与生命周期

Rust 入门系列(第 3 篇,共 5 篇) 前置阅读第 2 篇 - 基础语法与核心概念 阅读时间:30 分钟


引言

所有权(Ownership) 是 Rust 最独特也是最重要的特性。它让 Rust 在无需垃圾回收器的情况下,保证内存安全。

官方文档说:

“Ownership is Rust’s most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector.”

这一篇会详细讲解所有权、借用和生命周期,理解这些概念后,你会对 Rust 有全新的认识。


一、为什么需要所有权?

1.1 内存管理的三种方式

1. 手动管理(C/C++)

c
123
int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);  // 必须手动释放
  • ✅ 性能高,无运行时开销
  • ❌ 容易出错:内存泄漏、悬垂指针、双重释放

2. 垃圾回收(Java/Python/JavaScript)

java
12
String s = new String("hello");
// GC 自动回收,不需要 free
  • ✅ 安全,开发者无需关心
  • ❌ 性能开销:GC 暂停、内存占用高

3. 所有权系统(Rust)

rust
12
let s = String::from("hello");
// 离开作用域时自动释放
  • ✅ 安全:编译时检查
  • ✅ 高性能:无 GC,无运行时开销

1.2 常见内存问题

悬垂指针

c
1234
int* dangling() {
    int x = 42;
    return &x;  // ❌ x 已释放,返回无效指针
}

双重释放

c
123
int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr);  // ❌ 重复释放

内存泄漏

c
1234
void leak() {
    int* ptr = malloc(sizeof(int));
    // ❌ 忘记 free
}

Rust 的所有权系统在编译时防止这些问题。


二、所有权规则

根据 Rust Book,所有权有三条核心规则:

  1. 每个值都有一个所有者(owner)
  2. 同一时间只能有一个所有者
  3. 所有者离开作用域时,值被丢弃(drop)

2.1 作用域与所有权

rust
1234
{                       // s 无效,未声明
    let s = "hello";    // s 有效
    // 可以使用 s
}                       // s 离开作用域,不再有效

2.2 String 与堆内存

rust
12345
let s1 = String::from("hello");  // 堆分配
let s2 = s1;  // ⚠️ 移动(move),s1 不再有效

// println!("{}", s1);  // ❌ 编译错误:s1 已被移动
println!("{}", s2);     // ✅ OK

发生了什么?

  1. s1 拥有字符串
  2. s2 = s1 时,所有权转移s2
  3. s1 不再有效,避免双重释放

内存示意图

plaintext
12345
s1 (无效)
  |
  X (所有权已转移)

s2 -> [ptr, len, cap] -> 堆内存 ["hello"]

三、移动、克隆、复制

3.1 移动(Move)

rust
1234
let s1 = String::from("hello");
let s2 = s1;  // 移动

// s1 不再有效

规则:默认情况下,赋值会移动所有权。

3.2 克隆(Clone)

如果需要深拷贝,使用 .clone()

rust
1234
let s1 = String::from("hello");
let s2 = s1.clone();  // 克隆堆数据

println!("s1 = {}, s2 = {}", s1, s2);  // ✅ OK

开销clone() 会复制堆数据,有性能成本。

3.3 复制(Copy)

栈上的简单类型会自动复制:

rust
1234
let x = 5;
let y = x;  // 复制(不是移动)

println!("x = {}, y = {}", x, y);  // ✅ OK

实现了 Copy trait 的类型

  • 所有整数类型:i32, u64
  • 布尔类型:bool
  • 浮点类型:f32, f64
  • 字符类型:char
  • 元组(如果所有成员都实现 Copy):(i32, i32)

不能 Copy 的类型

  • String
  • Vec<T>
  • 任何分配堆内存的类型

四、函数与所有权

4.1 传递参数

rust
1234567891011121314151617
fn main() {
    let s = String::from("hello");
    takes_ownership(s);  // s 移动到函数中
    // println!("{}", s);  // ❌ s 不再有效

    let x = 5;
    makes_copy(x);  // x 被复制
    println!("{}", x);  // ✅ x 仍有效
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
}  // some_string 离开作用域,被 drop

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}  // some_integer 离开作用域,但无需 drop(栈上)

4.2 返回值与所有权

rust
1234567891011121314
fn gives_ownership() -> String {
    let s = String::from("hello");
    s  // 返回 s,所有权移出函数
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string  // 返回 a_string,所有权转移
}

fn main() {
    let s1 = gives_ownership();  // s1 获得所有权
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2);  // s2 移入,s3 获得所有权
}

五、引用与借用

5.1 不可变引用

使用 & 创建引用,不获取所有权

rust
123456789
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 借用 s1
    println!("'{}' 的长度是 {}", s1, len);  // ✅ s1 仍有效
}

fn calculate_length(s: &String) -> usize {
    s.len()
}  // s 离开作用域,但不 drop(没有所有权)

规则

  • 可以有多个不可变引用
  • 引用不能修改值

5.2 可变引用

使用 &mut 创建可变引用:

rust
123456789
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);  // "hello, world"
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

关键限制

rust
123456
let mut s = String::from("hello");

let r1 = &mut s;
// let r2 = &mut s;  // ❌ 同一时间只能有一个可变引用

println!("{}", r1);

5.3 借用规则

官方文档总结的规则:

  1. 同一时间,要么只有一个可变引用,要么有任意数量的不可变引用
  2. 引用必须始终有效(不能悬垂)

为什么?

  • 防止数据竞争(data race)
  • 保证内存安全

示例:编译错误

rust
1234567
let mut s = String::from("hello");

let r1 = &s;      // OK
let r2 = &s;      // OK
// let r3 = &mut s;  // ❌ 错误:已有不可变引用

println!("{} and {}", r1, r2);

作用域结束后可以创建新引用

rust
123456789
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这里不再使用

let r3 = &mut s;  // ✅ OK
println!("{}", r3);

六、悬垂引用

6.1 什么是悬垂引用?

c
12345
// C 语言的悬垂指针
int* dangle() {
    int x = 42;
    return &x;  // ❌ 返回局部变量的指针
}

6.2 Rust 如何防止?

rust
1234
fn dangle() -> &String {  // ❌ 编译错误
    let s = String::from("hello");
    &s
}  // s 被 drop,返回的引用无效

编译器报错

plaintext
1
error[E0106]: missing lifetime specifier

正确做法:返回所有权

rust
1234
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // 移动所有权
}

七、生命周期

7.1 为什么需要生命周期?

生命周期确保引用始终有效

rust
12345678910
{
    let r;                // -------+-- 'a
                          //        |
    {                     //        |
        let x = 5;        // -+--'b |
        r = &x;           //  |     |
    }                     // -+     |
                          //        |
    // println!("{}", r); // ❌ x 已销毁
}                         // -------+

7.2 函数中的生命周期

rust
1234567
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

编译错误

plaintext
1
error[E0106]: missing lifetime specifier

为什么? 编译器不知道返回的引用来自 x 还是 y,无法判断生命周期。

7.3 生命周期标注

rust
1234567
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

含义

  • 'a 是生命周期参数
  • 返回值的生命周期至少xy 中较短的一样长

使用示例

rust
12345678910
fn main() {
    let string1 = String::from("long string");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("{}", result);  // ✅ OK
    }
    // println!("{}", result);  // ❌ string2 已销毁
}

7.4 结构体中的生命周期

rust
1234567891011
struct ImportantExcerpt<'a> {
    part: &'a str,  // 引用的生命周期
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

含义ImportantExcerpt 实例的生命周期不能超过 part 引用的数据。

7.5 生命周期省略规则

Rust 编译器会自动推断生命周期,某些情况下可以省略:

规则 1:每个引用参数都有自己的生命周期

rust
123
fn foo(x: &i32, y: &i32)
// 等价于
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)

规则 2:只有一个输入生命周期时,赋给所有输出

rust
123
fn foo(x: &i32) -> &i32
// 等价于
fn foo<'a>(x: &'a i32) -> &'a i32

规则 3:方法中,如果有 &self&mut self,其生命周期赋给所有输出

rust
12345678910
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

八、常见错误与解决方案

8.1 值被移动后使用

错误

rust
123
let s = String::from("hello");
let s2 = s;
println!("{}", s);  // ❌ s 已移动

解决方案

rust
123456789
// 方案 1:克隆
let s = String::from("hello");
let s2 = s.clone();
println!("{} {}", s, s2);  // ✅ OK

// 方案 2:借用
let s = String::from("hello");
let s2 = &s;
println!("{} {}", s, s2);  // ✅ OK

8.2 同时有可变和不可变引用

错误

rust
1234
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;  // ❌ 已有不可变引用
println!("{} {}", r1, r2);

解决方案:分离作用域

rust
123456
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);  // r1 最后使用

let r2 = &mut s;  // ✅ OK(r1 已结束)
println!("{}", r2);

8.3 返回局部变量的引用

错误

rust
1234
fn dangle() -> &String {
    let s = String::from("hello");
    &s  // ❌ s 会被 drop
}

解决方案:返回所有权

rust
1234
fn no_dangle() -> String {
    let s = String::from("hello");
    s  // ✅ 移动所有权
}

九、实战示例

9.1 字符串切片

rust
1234567891011121314151617
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string);
    println!("{}", word);  // "hello"
}

9.2 防止悬垂引用

rust
123456
fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    // s.clear();  // ❌ 编译错误:已有不可变引用 word
    println!("{}", word);
}

十、总结

10.1 核心概念

  1. 所有权三原则

    • 每个值有一个所有者
    • 同一时间只有一个所有者
    • 所有者离开作用域时,值被丢弃
  2. 借用规则

    • 同一时间:一个可变引用 OR 多个不可变引用
    • 引用必须始终有效
  3. 生命周期

    • 确保引用始终有效
    • 编译器自动推断(大部分情况)
    • 必要时需要显式标注

10.2 何时用什么?

场景方法示例
转移所有权移动let s2 = s1;
深拷贝克隆let s2 = s1.clone();
只读访问不可变引用&s
修改值可变引用&mut s
返回值移动所有权return s;

10.3 学习建议

  1. 多写代码:编译器错误信息很友好,会告诉你怎么改
  2. 先理解规则:不要死记硬背,理解为什么这样设计
  3. 借用优先:能借用就不要移动所有权
  4. 相信编译器:编译通过 = 内存安全

十一、下一步

11.1 练习建议

  1. 实现一个函数,返回字符串中最长的单词
  2. 创建一个 struct,包含引用字段,练习生命周期标注
  3. 尝试故意写出会导致数据竞争的代码,看编译器如何阻止

11.2 推荐资源


系列预告

第 4 篇:《Rust 入门(四):实战 CLI 工具》

  • 构建一个文件搜索工具
  • 使用 clap 处理命令行参数
  • 文件 I/O 和错误处理

第 5 篇:《Rust 入门(五):Rust + WebAssembly》


参考资料


系列导航