2.3 Rust的所有权系统
所有权系统是 Rust 最独特、最核心的特性。它是一套由编译器在编译时检查的规则,用于管理内存和其他资源,从而无需垃圾回收器(GC)即可保证内存安全。理解所有权是掌握 Rust 的关键。
所有权规则
在 Rust 中,所有权系统遵循以下三条核心规则:
- 每个值在 Rust 中都有一个被称为其所有者的变量。
- 同一时间,一个值只能有一个所有者。
- 当所有者离开作用域时,该值将被丢弃(内存被释放)。
作用域与所有权
作用域是程序中一个项(item)有效的范围。当变量进入作用域时,它变得有效;当它离开作用域时,其拥有的资源会被自动释放。
{ // s 在这里无效,它尚未声明
let s = String::from("hello"); // s 从这里开始有效
// 使用 s
} // 此作用域已结束,s 不再有效,其内存被自动释放
在这个例子中,s 的所有者就是变量 s 本身。当代码块结束时,s 离开作用域,Rust 会自动调用一个特殊的函数 drop 来释放 s 所指向的堆内存。这被称为资源获取即初始化(RAII)。
移动(Move)
对于存储在堆上的复杂数据类型(如 String、Vec),赋值或传参默认会移动所有权,而不是进行深拷贝。
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移动到 s2
// println!("{}", s1); // 错误!s1 不再有效,因为其所有权已被移动
println!("{}", s2); // 正确,s2 现在是所有者
为什么 s1 会失效?因为 String 由三部分组成:指向堆内存的指针、长度和容量。当 s2 = s1 时,Rust 复制了这些栈上数据,但 s2 的指针指向了与 s1 相同的堆内存。为了避免在 s1 和 s2 离开作用域时两次释放同一块内存(双重释放错误),Rust 认为 s1 不再有效。这种操作被称为“移动”。
相比之下,对于存储在栈上的简单类型(如整数、布尔值),赋值会进行复制,因为它们的拷贝成本很低。
let x = 5;
let y = x; // x 仍然有效,因为 i32 实现了 Copy trait
println!("x = {}, y = {}", x, y);
克隆(Clone)
如果我们确实需要深度复制堆上的数据,可以使用 clone 方法。
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝堆内存
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效
clone 会显式地复制堆内存,因此 s1 和 s2 各自拥有独立的内存,可以同时有效。
函数与所有权
将值传递给函数与赋值类似,会发生移动或复制。
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权被移动到函数中
// println!("{}", s); // 错误!s 已失效
let x = 5;
makes_copy(x); // x 是 i32 类型,实现了 Copy,所以 x 仍然有效
println!("{}", x); // 正确
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // 这里 some_string 离开作用域,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // 这里 some_integer 离开作用域,无特殊操作
函数的返回值也可以转移所有权。
fn main() {
let s1 = gives_ownership(); // 返回值所有权被移动到 s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 所有权被移动,返回值所有权被移动到 s3
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 返回 some_string,所有权移出函数
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回 a_string,所有权移出函数
}
所有权与内存安全
所有权系统通过编译时检查,从根本上解决了以下内存安全问题:
- 悬垂指针:当所有者离开作用域后,其内存被释放,其他指针无法再访问该内存,因为所有权规则保证了只有所有者才能释放内存。
- 双重释放:同一块内存只能被其所有者释放一次,因为所有权只能被移动,不能复制。
- 缓冲区溢出:Rust 的数组和向量在运行时进行边界检查,防止越界访问。
总结
所有权系统是 Rust 管理内存的基石。它通过一套严格的规则,在编译时确保内存安全,无需垃圾回收器。理解“移动”、“克隆”以及作用域的概念,是编写安全、高效的 Rust 代码的第一步。在后续章节中,我们将学习“借用”和“引用”,它们提供了在不转移所有权的情况下访问数据的能力,进一步扩展了所有权的灵活性。
