4.1 所有权的基本概念
所有权(Ownership)是 Rust 语言最核心、最独特的特性。它是一套由编译器在编译时强制执行的内存管理规则,旨在确保内存安全,同时避免了垃圾回收(GC)带来的性能开销。理解所有权是掌握 Rust 的关键。
4.1.1 所有权的核心规则
Rust 的所有权系统遵循三条基本规则:
- 每个值在 Rust 中都有一个所有者(Owner)。
- 在任意时刻,一个值只能有一个所有者。
- 当所有者离开作用域时,这个值将被丢弃(Drop)。
让我们通过代码来理解这些规则。
4.1.2 作用域与所有权
作用域(Scope)是程序中一个项(item)有效的范围。在 Rust 中,作用域通常由花括号 {} 定义。
{ // s 在这里无效,它尚未声明
let s = "hello"; // s 从这里开始有效
// 使用 s
} // 此作用域结束,s 不再有效
当变量 s 进入作用域时,它变得有效。当它离开作用域时,它的值就会被丢弃,内存被自动释放。这个过程由编译器自动插入的 drop 函数调用完成,类似于 C++ 中的 RAII(资源获取即初始化)模式。
4.1.3 变量与数据交互的方式
所有权规则在变量与数据交互时表现得尤为明显。主要分为两种情况:移动(Move)和克隆(Clone)。
1. 移动(Move)
对于存储在堆上的复杂数据类型(如 String、Vec),将一个变量赋值给另一个变量时,会发生“移动”。
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移动到 s2
// println!("{}", s1); // 编译错误!s1 不再有效
println!("{}", s2); // 正常,s2 现在是所有者
为什么不是复制?
String由三部分组成:指向堆内存的指针、长度和容量。这部分数据存储在栈上。- 字符串的实际内容("hello")存储在堆上。
- 当
let s2 = s1时,Rust 复制了栈上的指针、长度和容量。这意味着s1和s2的指针指向了堆上的同一块内存。 - 如果同时释放
s1和s2,就会导致“双重释放”(double free)错误,这是严重的内存安全问题。 - 为了避免这个问题,Rust 认为
s1不再有效。因此,只有s2在离开作用域时会释放内存。这被称为“移动”。
2. 克隆(Clone)
如果你确实想要深度复制堆上的数据,而不仅仅是栈上的指针,可以使用 clone 方法。
let s1 = String::from("hello");
let s2 = s1.clone(); // 深拷贝堆上的数据
println!("s1 = {}, s2 = {}", s1, s2); // 两者都有效
clone 会复制堆上的数据,因此 s1 和 s2 各自拥有独立的内存。代价是性能开销更大。
3. 复制(Copy)
对于存储在栈上的简单数据类型(如整数 i32、布尔值 bool、浮点数 f64、字符 char、以及由它们组成的元组),赋值操作默认是“复制”。
let x = 5;
let y = x; // x 的值被复制到 y
println!("x = {}, y = {}", x, y); // 两者都有效
这些类型实现了 Copy trait(特征)。当一个类型实现了 Copy,将其赋值给另一个变量时,旧变量仍然有效。Rust 不允许一个类型同时实现 Copy 和 Drop。如果一个类型或其任何部分实现了 Drop,则不能实现 Copy。
4.1.4 所有权与函数
将值传递给函数与赋值类似。将变量传递给函数时,会发生移动或复制。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值被移动进函数
// 这里 s 不再有效
let x = 5; // x 进入作用域
makes_copy(x); // x 是 i32 类型,实现了 Copy,所以 x 仍然有效
println!("x = {}", x); // 正常
} // x 离开作用域,x 被丢弃。s 已经被移动,无需处理。
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,drop 被调用,内存被释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,无特殊操作
4.1.5 返回值与作用域
函数也可以将值的所有权返回。
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值移动给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动进函数,函数将返回值移动给 s3
} // s3 离开作用域,被丢弃。s1 离开作用域,被丢弃。s2 已被移动,无需处理。
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 提供了**引用(References)和借用(Borrowing)**机制,允许你在不转移所有权的情况下使用值。这将是下一节的核心内容。
4.1.6 总结
- 所有权是 Rust 管理内存的核心机制,无需垃圾回收器。
- 每个值有且只有一个所有者。
- 当所有者离开作用域,值被自动丢弃。
- 对于堆上的复杂类型,赋值默认是移动,旧变量失效。
- 使用
clone可以进行深拷贝。 - 栈上的简单类型(实现了
Copytrait)赋值默认是复制。 - 函数参数和返回值也会导致所有权的转移。
掌握所有权的移动规则,是理解 Rust 内存安全和编写高效、无数据竞争代码的第一步。
