Rust快速入门(2) - 所有权(OwnerShip)

所有权是 Rust 最独特的特性,它使 Rust 无需垃圾回收器就能保证内存安全。所有权系统由一组规则组成,编译器会在编译时检查这些规则。所有权的核心规则包括:

  1. Rust 中的每个值都有一个被称为其所有者的变量
  2. 值在任一时刻只能有一个所有者
  3. 当所有者离开作用域时,这个值将被丢弃

所有权有两个重要的 Trait, 只能选择实现其中一个:

  • Copy Trait: 数据赋值以后旧变量还能继续使用
  • Drop Trait: 数据赋值以后旧变量失效 (超过变量作用域以后会自动调用 Drop Trait)

1. 所有权的概念与规则

所有权是 Rust 管理内存的一套规则,它在编译时由编译器检查,不会对程序运行时产生性能开销。

1.1 所有权的核心规则

  • 每个值都有一个变量作为其所有者
  • 同一时刻只能有一个所有者
  • 当所有者离开作用域时,该值将被丢弃

1.2 变量作用域

作用域是程序中项目有效的范围。例如:

{                     // s 在此处无效
    let s = "hello";  // s 从此处开始有效
    // 使用 s
}                     // 此作用域结束,s 不再有效

1.3 内存与分配

Rust 中有两个主要的内存区域:栈(Stack)和堆(Heap):

  • :按获取顺序存储值,按相反顺序移除(后进先出)。所有存储在栈上的数据必须有已知的固定大小。操作栈数据快速高效。
  • :存储大小在编译时未知或可能变化的数据。在堆上分配内存时会请求一定的空间,分配器找到足够大的空间,标记为已使用,并返回指针。堆上数据的访问相对较慢。

2. 数据交互方式

2.1 移动(Move)

当我们将一个包含堆数据的变量赋值给另一个变量时,Rust 会执行”移动”操作而非拷贝:

let s1 = String::from("hello");
let s2 = s1; // s1 的所有权被移动到 s2,s1 不再有效

在这种情况下,s1 被认为不再有效,这样可以避免”二次释放”(double free)错误。

2.2 克隆(Clone)

如果我们确实需要深度复制堆上的数据而不仅是栈上的引用,可以使用 clone 方法:

let s1 = String::from("hello");
let s2 = s1.clone(); // 堆数据被复制

2.3 复制(Copy)

对于完全存储在栈上的简单标量值,赋值操作执行的是复制而非移动:

let x = 5;
let y = x; // x 仍然有效,因为整数类型实现了 Copy trait

实现了 Copy trait 的类型在赋值给另一个变量后仍然有效。包括:

  • 所有整数类型(i32u32 等)
  • 布尔类型(bool
  • 浮点类型(f64 等)
  • 字符类型(char
  • 仅包含实现了 Copy 的类型的元组,如 (i32, i32)

3. 所有权与函数

函数参数的传递和返回也遵循所有权规则:

fn main() {
    let s = String::from("hello");  // s 进入作用域
    takes_ownership(s);             // s 的值被移动到函数中
                                    // 之后 s 不再有效
    
    let x = 5;                      // x 进入作用域
    makes_copy(x);                  // x 应该被移动,但 i32 是 Copy 的
                                    // 所以 x 在之后仍可以使用
}

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 离开作用域,无特殊操作

3.1 返回值与所有权

返回值也可以转移所有权:

fn main() {
    let s1 = gives_ownership();     // gives_ownership 将返回值移动给 s1
    
    let s2 = String::from("hello"); // s2 进入作用域
    
    let s3 = takes_and_gives_back(s2); // s2 被移动到函数中,
                                        // 返回值移动到 s3
}

fn gives_ownership() -> String {
    let some_string = String::from("yours"); // some_string 进入作用域
    some_string // 返回 some_string 并移出给调用的函数
}

fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
    a_string // 返回 a_string 并移出给调用的函数
}

4. 引用与借用

为了解决反复传递所有权的繁琐问题,Rust 提供了”引用”机制,允许使用值但不获取其所有权。

4.1 引用规则

  • 在任意给定时刻,你只能拥有以下之一:
    • 一个可变引用
    • 任意数量的不可变引用
  • 引用必须总是有效的

4.2 不可变引用

通过 & 符号创建引用,允许你引用值而不获取其所有权:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // 这里 s 离开作用域,但因为它没有所有权,所以不会发生任何特殊情况

这种”创建引用”的行为被称为”借用”(borrowing)。

4.3 可变引用

允许修改借用的值,使用 &mut 创建:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

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

限制:在同一时间内,对同一数据只能有一个可变引用。这个限制可以防止数据竞争(data race)。

数据竞争出现在以下三个行为同时发生时:

  1. 两个或更多指针同时访问同一数据
  2. 至少有一个指针被用来写入数据
  3. 没有同步数据访问的机制

此外,不能同时拥有不可变引用和可变引用,因为可变引用可能会使不可变引用失效。

4.4 悬垂引用

Rust 编译器确保引用永远不会变成悬垂引用(指向已被释放的内存位置的指针):

fn dangle() -> &String { // 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃,其内存被释放
  // 危险!

这段代码会导致编译错误,因为 s 在函数结束时被释放,返回的引用将指向无效内存。

5. 切片(Slice)类型

切片允许你引用集合中的一部分连续元素序列,而不是整个集合。切片是一种引用,没有所有权。

5.1 字符串切片

字符串切片是对 String 部分内容的引用,看起来像这样:

let s = String::from("hello world");
let hello = &s[0..5];  // "hello"
let world = &s[6..11]; // "world"

字符串切片的类型标记为 &str

5.2 字符串字面量是切片

字符串字面量的类型实际上是 &str

let s = "Hello, world!"; // s 的类型是 &str

这是一个指向二进制程序特定位置的切片,这也是字符串字面量不可变的原因。

5.3 函数参数中的字符串切片

将函数参数定义为接收字符串切片可以使 API 更加通用:

fn first_word(s: &str) -> &str {
    // 实现
}

// 可以同时接受 String 引用或切片
let my_string = String::from("hello world");
let word1 = first_word(&my_string[..]);  // 用于整个 String 的切片
let word2 = first_word(&my_string);      // 用于 String 的引用

// 也可以接受字符串字面量或其切片
let my_string_literal = "hello world";
let word3 = first_word(&my_string_literal[..]);
let word4 = first_word(my_string_literal); // 字符串字面量已经是切片了

5.4 其他类型的切片

切片不仅限于字符串。例如,数组也可以有切片:

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32]

6. 总结

Rust 的所有权、借用和切片概念确保了程序在编译时的内存安全性,无需垃圾回收器或手动内存管理。所有权系统的关键优势在于:

  1. 内存自动释放:当值的所有者离开作用域时,值会自动被清理,避免了内存泄漏
  2. 编译时检查:所有权规则在编译时由编译器强制执行,防止了许多常见的内存错误
  3. 零成本抽象:所有权系统在运行时没有额外开销,保持了高性能
  4. 消除数据竞争:借用规则防止了并发程序中的数据竞争

所有权是 Rust 的基础,它影响了 Rust 中许多其他功能的工作方式,是理解和掌握 Rust 编程的关键概念。通过深入理解所有权,我们能够编写既安全又高效的 Rust 代码。


好好学习,天天向上