Rust —— 所有权

Rust 最独特的特性之一就是 所有权(Ownership),它让 Rust 在没有垃圾回收器(GC)的情况下仍能保证内存安全。理解所有权的工作原理非常重要。

1. 所有权的基本概念

所有权是一组规则,用来控制 Rust 如何管理内存。所有程序都需要管理运行时如何使用内存。常见的几种方式:

  • 垃圾回收(GC):例如 Java、Go,自动跟踪和清理不再使用的内存。
  • 手动管理:例如 C++,程序员必须显式分配和释放内存。
  • 所有权机制:Rust 使用所有权系统,在编译期检查规则,不影响运行时性能。

所有权规则(简要)

  1. 每个值都有一个所有者(owner)。
  2. 对于不实现 Copy 的类型(如 String),同一时间只能有一个所有者(即移动语义)。
  3. 当所有者离开作用域时,值会被自动丢弃,内存释放。

注意:对于实现了 Copy trait 的简单类型(如整型、浮点、布尔、字符,以及不含堆数据的元组),赋值会复制 Copy,而不是 Move


2. 变量的作用域

变量的生命周期与作用域相关:

1
2
3
4
5
6
7
fn main() {
{ // s 在此处无效,还未声明
let s = "hello"; // s 从这里开始有效

// 使用 s
} // 作用域结束,s 无效
}

3. String 类型

字符串字面量(如 "hello")被硬编码到程序中,不可变。如果需要可变或运行时获取的字符串,可以使用 String,它在堆上分配内存。

1
2
3
4
5
6
7
fn main() {
let mut s = String::from("hello");

s.push_str(", world!"); // 在 String 后追加文本

println!("{s}"); // 输出 "hello, world!"
}

4. 内存与分配

String 的工作流程:

  1. 在调用 String::from 或其它分配时,向堆请求内存。
  2. 当变量超出作用域时,Rust 自动调用 drop,释放该内存。
1
2
3
4
5
fn main() {
{
let s = String::from("hello"); // s 有效
} // s 超出作用域,自动调用 drop,内存释放
}

5. Move 与 Clone

Move(移动语义)

对于不实现 Copy 的类型,赋值或传参会发生 move

1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移到 s2,s1 不再有效
// println!("{s1}"); // 编译错误:s1 无效
}

这样设计是为了防止出现 二次释放(double free) 的安全问题(两个变量同时指向同一堆内存,且各自尝试释放)。

Clone(深拷贝)

如果需要真正复制堆上的数据,使用 clone()

1
2
3
4
5
6
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝

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

Copy(栈上数据的自动复制)

实现了 Copy 的类型在赋值时会按位复制(不会发生 Move),例如:整数、浮点、布尔、字符,以及不包含堆数据的元组。

1
2
3
4
5
6
fn main() {
let x = 5;
let y = x; // x 被 Copy,所以仍然可用

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

6. 函数传值与返回值

传参时的所有权行为

将值传给函数时也会发生 Move 或 Copy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值被 move 到函数,s 在这里不再有效

let x = 5; // x 进入作用域(i32 实现了 Copy)
makes_copy(x); // x 被 Copy,依然可用
println!("{x}");
}

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

fn makes_copy(some_integer: i32) {
println!("{some_integer}");
} // some_integer 离开作用域,无需特殊处理

如果你在 takes_ownership(s); 之后再使用 s,会编译错误,因为 s 的所有权已被移动。

返回值也会移动所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let s1 = gives_ownership(); // gives_ownership 返回一个 String,所有权转给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被 move 到函数里,函数再返回所有权给 s3
} // s3 被 drop,s2 已被 move,不再有效,s1 被 drop

fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 返回所有权
}

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

注意: 如果不想在函数间频繁移动所有权,通常可以使用引用(借用)。


7. 引用与借用(References & Borrowing)

引用是指向数据的指针,但不拥有数据:

不可变引用

1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 将 s1 的不可变借用传入函数
println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
s.len()
} // s 是引用,不会影响所有权,也不会在此处释放 s1

可变引用

默认引用是不可变的。若要修改被借用的数据,使用可变引用 &mut

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("hello");
change(&mut s);
}

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

可变引用的限制:在任一给定时间,对某个值最多只能有一个可变引用。这避免了并发写入导致的数据竞争。数据竞争会被以下三种行为触发:

  • 两个或多个指针同时访问同一块内存。

  • 至少有一个指针正在写入数据。

  • 访问没有任何同步机制保护。

错误示例:多个可变引用

1
2
3
4
let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s; // ❌ 编译错误

通过作用域解决

1
2
3
4
5
6
7
let mut s = String::from("hello");

{
let r1 = &mut s;
} // r1 离开作用域

let r2 = &mut s; // ✅ 可以

不可变引用与可变引用不能同时存在:如果有不可变引用仍在使用,则不能创建可变引用;但如果不可变引用的作用域结束且不再使用,就可以随后创建可变引用。

示例如下:

可变引用与不可变引用冲突

1
2
3
4
5
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
let r3 = &mut s; // ❌ 编译错误

合理利用作用域

1
2
3
4
5
6
7
8
9
10
fn main() {
let mut s = String::from("hello");

let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1 和 r2 的使用在这里结束

let r3 = &mut s; // 现在可以创建可变引用
println!("{r3}");
}

8. 悬垂引用(Dangling References)

悬垂引用是指引用指向的值已被释放,但引用仍被使用。Rust 的所有权系统和借用规则在编译期防止了悬垂引用。

错误示例(编译失败)

1
2
3
4
5
6
7
8
fn dangle() -> &String {
let s = String::from("hello");
&s // ❌ 返回引用到一个局部变量,s 离开作用域后被 drop,引用悬空
}

fn main() {
let reference_to_nothing = dangle();
}

正确做法:返回值的所有权(而不是引用),或确保引用指向的值在调用者作用域内保持有效:

1
2
3
4
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回所有权,安全
}

9. 切片(Slice)

切片是对集合(如字符串、数组)中连续元素的引用,不取得所有权

字符串切片

1
2
3
4
5
6
7
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];
println!("{hello}, {world}");
}

简写与尾部省略:

1
2
3
4
5
6
7
8
9
10
let s = String::from("hello");
let slice1 = &s[0..2];
let slice2 = &s[..2]; // 等效

let len = s.len();
let slice3 = &s[3..len];
let slice4 = &s[3..]; // 等效

let all = &s[0..len];
let all2 = &s[..]; // 等效

数组切片

1
2
3
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

总结

  • 所有权是 Rust 的核心机制,用以在编译期保证内存安全。
  • 了解 MoveCloneCopy 的区别有助于写出高效且无内存错误的代码。
  • 引用(借用)允许临时使用数据而不转移所有权,且借用规则在编译期防止数据竞争和悬垂引用。
  • 切片提供对集合部分的安全引用,不会取得所有权。

Hooray!所有权学习完成!!!