Rust · #rust#ownership#borrowing

Rust所有权与借用检查器详解

2025.07.23 Rust 8 min 3.1k
// 目录 · contents

前言

Rust的所有权系统是其最核心的特性,通过编译期检查实现内存安全,无需垃圾回收器。所有权、借用和生命周期三者协同工作,让Rust在保证安全的同时保持零成本抽象。本文将深入解析这些机制的原理和使用模式。

所有权规则

Rust的三条所有权规则:

  1. 每个值都有一个所有者(owner)
  2. 同一时刻只能有一个所有者
  3. 当所有者离开作用域,值被丢弃(drop)
graph TB
    subgraph "栈内存 (Stack)"
        S1[s1: ptr, len, cap]
        S2[s2: ptr, len, cap]
    end

    subgraph "堆内存 (Heap)"
        H1["hello"]
    end

    S1 -.->|"移动后s1无效"| H1
    S2 -->|"s2拥有数据"| H1

    style S1 fill:#e0e0e0,color:#999
1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
// String类型存储在堆上
let s1 = String::from("hello");

// 移动(move),s1不再有效
let s2 = s1;

// println!("{}", s1); // 编译错误:value borrowed here after move

println!("{}", s2); // OK: s2是所有者
}
// s2离开作用域,堆内存被释放

移动语义(Move Semantics)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 移动发生的场景

// 1. 赋值
let a = String::from("hello");
let b = a; // a被移动

// 2. 函数传参
fn take_string(s: String) {
println!("{}", s);
} // s在这里被drop

let s = String::from("world");
take_string(s); // s被移动到函数内
// println!("{}", s); // 编译错误

// 3. 函数返回值
fn give_string() -> String {
let s = String::from("returned");
s // 所有权转移给调用者
}
let s = give_string(); // s获得所有权

// 4. Copy trait - 栈上的简单类型不移动,而是复制
let x: i32 = 42;
let y = x; // 复制,x仍然有效
println!("x={}, y={}", x, y); // OK

// 实现了Copy的类型:
// 所有整数、浮点、bool、char
// 元组(如果所有元素都实现了Copy)
// 数组(如果元素实现了Copy)

Clone vs Copy

graph LR
    subgraph "Copy (栈上复制)"
        I1["i32: 42"] -->|位拷贝| I2["i32: 42"]
        NOTE1[零成本,编译器自动处理]
    end

    subgraph "Clone (显式深拷贝)"
        S1["String: ptr→heap"] -->|clone| S2["String: ptr→new heap"]
        NOTE2[需要显式调用,可能昂贵]
    end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Clone: 显式深拷贝
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,s1仍然有效
println!("s1={}, s2={}", s1, s2);

// 自定义类型
#[derive(Clone)]
struct Config {
name: String,
values: Vec<i32>,
}

let config1 = Config {
name: String::from("test"),
values: vec![1, 2, 3],
};
let config2 = config1.clone(); // 深拷贝

借用(Borrowing)

借用允许在不获取所有权的情况下使用值。

不可变借用(&T)

1
2
3
4
5
6
7
8
9
10
fn calculate_length(s: &String) -> usize {
s.len()
// s是借用,函数结束时不会drop原始数据
}

fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 借用s
println!("'{}' has length {}", s, len); // s仍然有效
}

可变借用(&mut T)

1
2
3
4
5
6
7
8
9
fn append_world(s: &mut String) {
s.push_str(" world");
}

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

借用规则

graph TB
    RULE[借用规则] --> R1[规则1: 任意数量的不可变借用<br>&T + &T + &T = OK]
    RULE --> R2[规则2: 同一时刻只能有一个可变借用<br>&mut T = OK<br>&mut T + &mut T = ERROR]
    RULE --> R3[规则3: 不可变和可变借用不能共存<br>&T + &mut T = ERROR]

    style R1 fill:#388e3c,color:#fff
    style R2 fill:#f57c00,color:#fff
    style R3 fill:#d32f2f,color:#fff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn main() {
let mut s = String::from("hello");

// 多个不可变借用 - OK
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);

// 可变借用 - OK (r1, r2已经不再使用)
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);

// 不可变和可变同时存在 - ERROR
// let r4 = &s;
// let r5 = &mut s;
// println!("{}", r4); // 如果r4在r5之后使用,编译错误
}

悬垂引用(Dangling Reference)

1
2
3
4
5
6
7
8
9
10
11
// Rust编译器阻止悬垂引用
fn dangle() -> &String { // 编译错误
let s = String::from("hello");
&s // s在函数结束时被drop,引用将指向无效内存
}

// 正确做法:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 移动所有权给调用者
}

生命周期(Lifetimes)

生命周期注解告诉编译器多个引用之间的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 编译器无法推断返回值的生命周期
// 返回值的引用来自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");

{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("Longest: {}", result);
}
// result不能超出string2的生命周期
}

生命周期省略规则(Lifetime Elision)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 编译器自动推断生命周期的三条规则:

// 规则1: 每个输入引用获得独立的生命周期
// fn foo(x: &str, y: &str) -> 推断为 -> fn foo<'a, 'b>(x: &'a str, y: &'b str)

// 规则2: 只有一个输入引用时,输出生命周期 = 输入生命周期
fn first_word(s: &str) -> &str { /* ... */ }
// 等同于: fn first_word<'a>(s: &'a str) -> &'a str

// 规则3: 方法中有&self时,输出生命周期 = &self的生命周期
impl Config {
fn name(&self) -> &str {
&self.name
}
// 等同于: fn name<'a>(&'a self) -> &'a str
}

// 当三条规则都无法确定时,需要显式标注
fn longest(x: &str, y: &str) -> &str { /* 编译错误,需要手动标注 */ }

结构体中的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 结构体包含引用时必须标注生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}

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

// 规则3: 返回值生命周期 = &self
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}

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

’static生命周期

1
2
3
4
5
6
// 'static表示引用在整个程序运行期间都有效
let s: &'static str = "I have a static lifetime";
// 字符串字面量都是'static的,它们存储在程序的二进制文件中

// 注意:不要随意使用'static来解决生命周期错误
// 这通常意味着设计有问题

NLL(Non-Lexical Lifetimes)

Rust 2018引入的NLL让借用检查更加精确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Rust 2015: 借用在整个词法作用域有效
fn example_old() {
let mut data = vec![1, 2, 3];
let first = &data[0]; // 借用开始
// 在Rust 2015中,first的借用延续到作用域结束
// data.push(4); // 2015编译错误,即使first之后不再使用
println!("{}", first);
// Rust 2015: first的借用在这里仍然"活着"
}

// Rust 2018+ (NLL): 借用在最后一次使用后结束
fn example_nll() {
let mut data = vec![1, 2, 3];
let first = &data[0]; // 借用开始
println!("{}", first); // 借用最后一次使用
// NLL: first的借用到这里就结束了
data.push(4); // OK! 不再有活跃的不可变借用
println!("{:?}", data);
}
graph TB
    subgraph "Lexical Lifetimes (旧)"
        L1["let first = &data[0]; // 借用开始"] --> L2["println!(first);"]
        L2 --> L3["data.push(4); // ERROR: 借用仍在作用域内"]
        L3 --> L4["} // 借用在这里结束"]
    end

    subgraph "Non-Lexical Lifetimes (NLL)"
        N1["let first = &data[0]; // 借用开始"] --> N2["println!(first); // 借用在这里结束"]
        N2 --> N3["data.push(4); // OK: 借用已结束"]
        N3 --> N4["}"]
    end

    style L3 fill:#d32f2f,color:#fff
    style N3 fill:#388e3c,color:#fff

常见模式

模式1: 字符串处理

1
2
3
4
5
6
7
8
9
10
// 使用&str而非&String作为函数参数
fn greet(name: &str) { // 接受&str和&String
println!("Hello, {}!", name);
}

let owned = String::from("World");
let literal = "World";

greet(&owned); // &String自动解引用为&str
greet(literal); // &str直接传入

模式2: 迭代器与借用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fn main() {
let names = vec![
String::from("Alice"),
String::from("Bob"),
String::from("Charlie"),
];

// iter()返回&T,不消费原集合
for name in names.iter() {
println!("Hello, {}", name);
}
println!("Still have {} names", names.len()); // OK

// into_iter()消费集合,获取所有权
for name in names.into_iter() {
println!("Goodbye, {}", name);
}
// println!("{:?}", names); // 编译错误:names已被消费

// iter_mut()获取可变引用
let mut scores = vec![1, 2, 3];
for score in scores.iter_mut() {
*score *= 2;
}
println!("{:?}", scores); // [2, 4, 6]
}

模式3: 内部可变性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::cell::RefCell;

// RefCell: 运行时借用检查(单线程)
struct Logger {
messages: RefCell<Vec<String>>,
}

impl Logger {
fn new() -> Self {
Logger {
messages: RefCell::new(Vec::new()),
}
}

// 即使&self是不可变引用,也能修改messages
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(String::from(msg));
}

fn dump(&self) {
for msg in self.messages.borrow().iter() {
println!("{}", msg);
}
}
}

模式4: Cow(Clone on Write)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::borrow::Cow;

// Cow: 避免不必要的克隆
fn process_name(name: &str) -> Cow<str> {
if name.contains(' ') {
// 需要修改,返回拥有的String
Cow::Owned(name.replace(' ', "_"))
} else {
// 不需要修改,返回借用
Cow::Borrowed(name)
}
}

let name1 = process_name("hello"); // Borrowed, 无分配
let name2 = process_name("hello world"); // Owned, 有分配

常见编译错误和解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 错误1: 同时存在可变和不可变借用
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // 错误!
println!("{}", first);
// 解决: 将println移到push之前

// 错误2: 从借用的值中移动
let v = vec![String::from("hello")];
let s = v[0]; // 错误!不能从Vec中移动
// 解决: 使用clone或引用
let s = v[0].clone(); // 或 let s = &v[0];

// 错误3: 返回局部变量的引用
fn bad() -> &str {
let s = String::from("hello");
&s // 错误!s会被drop
}
// 解决: 返回所有权
fn good() -> String {
String::from("hello")
}

// 错误4: 闭包捕获可变引用
let mut data = vec![1, 2, 3];
let closure = || data.push(4); // 闭包捕获&mut data
data.push(5); // 错误!data已被闭包借用
closure();
// 解决: 调整使用顺序,先用完data再创建闭包

总结

Rust所有权系统的核心要点:

  1. 所有权:每个值只有一个所有者,离开作用域时自动释放
  2. 移动语义:赋值和传参默认移动所有权(Copy类型除外)
  3. 借用规则:同时只能有多个不可变借用或一个可变借用
  4. 生命周期:确保引用不会超过被引用数据的生存期
  5. NLL:借用在最后一次使用后结束,而非词法作用域结束
  6. 实用模式&str优于&StringCow避免不必要克隆、RefCell提供内部可变性

所有权系统是Rust学习曲线最陡峭的部分,但一旦理解,它就是编写安全高效代码的利器。编译器不是你的敌人,它是在编译期发现了运行时才会暴露的内存错误。

作者 · authorzt
发布 · date2025-07-23
篇幅 · length3.1k 字 · 8 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论