Skip to content

Rust - 所有权

在现代系统编程中,正确管理内存对于性能和安全性至关重要。Rust 采用一种独特的概念——**所有权(Ownership)来解决这个问题。要理解所有权,我们首先必须了解程序通常是如何通过栈(Stack)和堆(Heap)**来管理内存的。

栈用于存放编译时大小已知且固定的数据。它速度极快,因为它是一个简单的“后进先出”(LIFO)结构。可以将其想象成一叠盘子:你将新盘子放在最上面,也从最上面取走盘子。所有标量类型(scalar types),如 i32、f64、bool 和 char 都存储在栈上。

堆用于存放编译时大小未知或可能改变的数据,包括像 String 或 Vec<T> 这种可以动态增长的类型。当你需要在堆上分配空间时,你会向操作系统请求,操作系统会返回一个你存储在栈上的指针(pointer)(即内存地址)。这个过程称为“分配(allocation)”,比压入栈(push to the stack)要慢。堆的组织方式不如栈有序,更像是寻找停车场里的空位。

所有权是 Rust 解决内存管理问题的方案,它替代了垃圾回收器或手动内存管理(malloc/free)的需求。所有权由编译器强制执行的一组简单规则来管理:

  • 一个所有者: Rust 中的每个值都只有一个变量是其“所有者”。
  • 一次一个地方: 在任何给定时间,一个值只能有一个所有者。
  • 超出作用域即丢弃: 当所有者超出其作用域时,它拥有的值会自动被丢弃(即其内存被释放)。

让我们看看这些规则是如何运作的。当我们写 let s = String::from("hello"); 时,s 是堆上分配的字符串数据 "hello" 的所有者。

当我们将一个堆分配的值从一个变量赋值给另一个变量时,Rust 会执行一次移动(move)。这并非浅拷贝(shallow copy);所有权被转移了。

fn main() {
// s1 拥有在堆上分配的 String 数据。
let s1 = String::from("hello");
// String 数据的所有权从 s1 移动到 s2。
// s1 不再有效,无法使用。
let s2 = s1;
// 这将导致编译时错误:借用了已移动的值:`s1`
// println!("s1 is: {}", s1);
println!("s2 is: {}", s2); // 这是有效的。
}

这可以防止“二次释放(double free)”错误,即两个变量在超出作用域时尝试释放同一块内存。通过使 s1 失效,Rust 在编译时保证了内存安全。

将值传递给函数也会移动所有权。同样的规则适用。

fn main() {
let s = String::from("world"); // s 进入作用域
// s 的值被移动到函数中...
// ... s 在这里不再有效。
takes_ownership(s);
let x = 5; // x 进入作用域
// x 是一个只在栈上的类型,因此它被复制,而不是移动。
makes_copy(x);
println!("x is still accessible: {}", x); // x 仍然可以访问:{},这没问题!
} // 在这里,x 超出作用域。`s` 已经被移动。
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 在这里,some_string 超出作用域并调用了 `drop`。
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 在这里,some_integer 超出作用域。没有发生特殊的事情。

类似地,从函数返回一个值也会将所有权转移给接收结果的变量。

那么为什么在将 x 赋值给函数后,我们仍然可以使用它呢?这是因为简单的标量类型实现了 Copy trait。如果一个类型实现了 Copy,那么变量会被复制而不是移动。这是可行的,因为它们的大小已知且完全存储在栈上,使得复制操作非常廉价。

  • 所有整数类型,如 u32。
  • 布尔类型,bool。
  • 所有浮点类型,如 f64。
  • 字符类型,char。
  • 元组(Tuples),如果它们只包含也实现了 Copy 的类型。

不断地来回传递所有权可能会很繁琐。Rust 有一个强大的特性,称为借用(borrowing),它允许你在不取得值所有权的情况下使用它。我们使用 & 符号创建值的引用(reference)。

fn main() {
let s1 = String::from("Rust is powerful!");
// 我们传递 s1 的引用,因此不会移动所有权。
let len = calculate_length(&s1);
// s1 在这里仍然有效!
println!("The length of '{}' is {}.", s1, len);
}
// 函数签名接受一个 String 的引用。
fn calculate_length(s: &String) -> usize {
s.len()
} // 在这里,s 超出作用域。但因为它不拥有所有权,
// 所以什么都不会发生。原始的 `s1` 不会被丢弃。

所有权、移动和借用这些概念是 Rust 无需垃圾回收器即可提供内存安全性的基石,从而实现了无与伦比的性能和可靠性。