Rust 的所有权(Ownership)规则
Rust 的 所有权(Ownership) 是该语言最核心的特性,也是 Rust 能够在没有垃圾回收(GC)的情况下保证内存安全的关键机制。
简单来说,所有权是一套编译时(Compile-time)检查的规则。如果违反了这些规则,程序将无法通过编译。
以下是 Rust 所有权系统的核心内容:
1. 三大核心规则 (The Three Rules)
这是必须死记硬背的铁律:
- Rust 中的每一个值都有一个被称为其“所有者”(Owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域(Scope)时,这个值将被丢弃(Drop)。
2. 规则详解与示例
为了理解这些规则,我们需要区分栈(Stack)和堆(Heap)上的数据。
A. 作用域规则 (Scope)
当变量离开代码块 { ... } 时,Rust 会自动调用 drop 函数来释放内存。
plaintext
{ // s 在这里无效,它还没声明
let s = "hello"; // s 在这里有效
// 使用 s
} // 此作用域结束,s 不再有效
B. 移动 (Move) —— 针对堆数据
这是 Rust 与 C++ 或 Python 最不同的地方。
对于复杂类型(如 String),数据存储在堆上。当你把一个变量赋值给另一个变量时,所有权会发生转移。
plaintext
let s1 = String::from("hello");
let s2 = s1;
// println!("{}, world!", s1); // ❌ 报错!s1 已经失效了
println!("{}, world!", s2); // ✅ 正确
- 发生了什么?
s1指向堆上的 "hello"。- 当执行
let s2 = s1;时,Rust 没有复制堆上的数据(深拷贝),而是复制了指针。 - 关键点: 为了保证内存安全(避免二次释放 double-free),Rust 规定
s1不再有效。这被称为 移动(Move)。
C. 克隆 (Clone)
如果你确实想要深度复制堆上的数据,必须显式调用 .clone()。
plaintext
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝,堆上现在有两份 "hello"
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两个都有效
D. 拷贝 (Copy) —— 针对栈数据
对于简单类型(如整数、浮点数、布尔值、字符),它们的大小固定且存储在栈上。
plaintext
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y); // ✅ 两个都有效
- 为什么 x 没失效?
- 因为整数实现了
Copytrait(特征)。 - 对于这些类型,赋值操作是直接在栈上进行位拷贝,速度极快,不需要“移动”所有权。
- 因为整数实现了
3. 所有权与函数
将值传递给函数,语义上等同于给变量赋值。这意味着所有权会转移(除非是 Copy 类型)。
plaintext
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到了函数里
// ... 所以在这里 s 不再有效
// println!("{}", s); // ❌ 报错!s 已经被拿走了
let x = 5; // x 进入作用域
makes_copy(x); // x 是 Copy 类型
// ... 所以 x 在这里依然有效
}
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里 some_string 离开作用域,`drop` 被调用,内存释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
}
4. 借用 (Borrowing) —— 引用
如果每次使用变量都要转移所有权再转移回来(通过返回值),会非常麻烦。Rust 提供了 引用(References),使用 & 符号。
借用允许你使用一个值,但不获取其所有权。
plaintext
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递引用,不转移所有权
println!("The length of '{}' is {}.", s1, len); // ✅ s1 依然有效
}
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里 s 离开作用域,但因为它没有所有权,所以什么也不会发生(不释放内存)
引用的规则(借用规则)
这也是所有权系统的一部分:
- 在任意给定时间,你要么只能有一个可变引用 (
&mut T),要么只能有任意数量的不可变引用 (&T)。 - 引用必须总是有效的(不能有悬垂指针)。
总结
Rust 的所有权规则是为了解决以下问题:
- 内存泄漏:通过作用域规则自动释放。
- 二次释放 (Double Free):通过“移动”语义,确保只有一个所有者负责释放。
- 数据竞争 (Data Race):通过借用规则(读写互斥),在编译阶段杜绝多线程数据竞争。
虽然刚开始学习时会觉得编译器在“刁难”你(Fighting the borrow checker),但一旦习惯,它能极大地提高代码的安全性和质量。