我必须立刻押注 Rust 之二🎲:一文通关所有权(Ownership)系统

432 阅读5分钟

Rust 最独特的地方之一就是它的 所有权(Ownership) 系统。

它主要用于管理内存和资源的安全性,防止内存泄漏和数据竞争。

通过这一系统,Rust 不需要垃圾回收器来管理内存,而是通过编译时的检查确保内存安全。

所有权规则

Rust 的所有权规则有三条核心原则:

  • 每个值在 Rust 中都有一个所有者
  • 每个值在同一时间只能有一个所有者。
  • 当所有者离开作用域时,值会被丢弃,内存也会自动释放。

示例一

fn main() {
    let s1 = String::from("hello");  // s1 是所有者
    let s2 = s1;  // s1 的所有权被转移给 s2

    // println!("{}", s1);  // 编译错误,s1 不再拥有该值
    println!("{}", s2);  // 正常输出
}

这里的赋值操作会导致所有权的转移。

s1的所有权被转移到s2,之后s1不再有效。

示例二

let s1 = String::from("hello");
fn arr_self (s: String) {
    println!("{:?}", s);
}
arr_self(s1);
println!("{:?}", s1); // 不能再使用s1

函数参数传递也可以看作是一个赋值过程。

这里s1的所有权被转移给函数arr_self,之后 s1 不再有效。

Move vs Copy

在 Rust 中,不是所有类型的值都会在所有权转移时被“移动”(Move),有些类型会被“复制”(Copy)

Copy 类型

i32boolchar 等大小固定、简单的类型可以实现 Copy 特性。

它们在赋值或传参时会被复制,而不是移动。这些类型不涉及所有权转移。

示例一

let x = 5;
let y = x;  // x 被复制,而不是移动
println!("x: {}, y: {}", x, y);  // 两者都可以使用

为什么 i32 等类型看起来像没有所有者 ?

正是因为这个 Copy 机制,它们在赋值或传递时不会转移所有权,而是直接复制。

因此,它们在赋值时没有表现出显式的所有权转移行为,给人的感觉是“它们没有所有者”。

回到所有权的三条规则

  • 每个值在 Rust 中都有一个所有者

    适用于所有类型的,也包括 i32、bool 等。即使它们可以被复制,每个副本也有自己的所有者

  • 每个值在同一时间只能有一个所有者

    对于 Copy 类型,它们在被复制后,副本与原本是独立的,因此每个副本都有自己的所有者。

  • 当所有者离开作用域时,值会被丢弃

    对于 Copy 类型,它们会在离开作用域时被删除。由于它们是栈上的小数据,不涉及堆内存的管理;对于堆分配的数据类型,Rust 会自动释放它们的内存。

示例二

let arr = [1, 2, 3, 4, 5];
fn arr_self (arr: [i32; 5]) {
    println!("{:?}", arr);
}

arr_self(arr);
println!("{:?}", arr); // 有效

对于类型 [i32; 5] 来说,它是一个固定大小的数组,实现了 Copy 特性。 因此,当传递给函数 arr_self 时,会发生复制,而不是所有权转移。 所以,arr 在函数内部和外部都是有效的。

Move 类型

StringVec 等堆上分配的类型,

在赋值或传参时会发生所有权转移(move)。原来的变量将失效。

示例一

let s1 = String::from("hello");
let s2 = s1;  // s1 的所有权被转移给 s2
// println!("{}", s1);  // 错误:s1 不再有效
println!("{}", s2);

示例二

let arr = [
    String::from("hello"),
    String::from("world"),
];
fn arr_self (arr: [String; 2]) {
    println!("{:?}", arr);
}
arr_self(arr);
println!("{:?}", arr); // 编译错误

对于类型 [String; 2] 来说,它是一个固定大小的数组。 但它的元素是 String 类型,它们在堆上分配内存,因此不实现 Copy 特性。

避免所有权转移

引用

如果你不想转移所有权,而是希望多个变量共享同一个值,可以使用引用(Reference)

通过 & 来创建引用,默认引用是不可变的,可以通过 &mut 创建可变引用。

不可变引用

示例一

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;  // 引用 s1 的值

    println!("s1: {}, s2: {}", s1, s2);  // s1 和 s2 都能正常使用
}

示例二

let s1 = "hello"; // &str
let s2 = s1;
println!("s1: {}, s2: {}", s1, s2);  // s1 和 s2 都能正常使用

&str 是字符串字面量,也是一个不可变引用。

它不拥有数据,只是引用常量字符串,因此不涉及所有权的问题。

可变引用

示例一

fn main() {
    let mut s1 = String::from("hello");
    let s2 = &mut s1;  // 可变引用
    s2.push_str(", world");
    println!("s2: {}", s2); // s2: hello, world
    println!("s1: {}", s1); // s1: hello, world
}

注意:这里 s1 的打印不能在 s2 之前, 这涉及到 Rust 的借用规则,将在下一节中提及

解引用

如果你想要访问引用所指向的值,可以使用**解引用(Dereference)**操作符 *

fn main() {
    let mut x = 10;  // 可变的 i32 值
    let y = &mut x;  // 创建一个对 x 的可变引用

    *y = *y + 5;  // 解引用并修改 y 所指向的值

    println!("Value of x: {}", x); // 输出 15
}

yx 的可变引用, *y (解引用 y) 实际上是对 x 的直接访问

解引用也不会改变所有权, 因此原始数据的所有权保持不变属于 x

  • 所有权系统 是 Rust 语言的一个核心特性,旨在通过严格的规则在编译时捕获潜在的内存问题

  • 转移复制 是 Rust 数据传递的两种机制。Copy 类型不会引起所有权转移,而是会复制数据

  • 引用 允许多个地方共享同一数据,能够避免转移所有权,从而减少性能开销和内存安全问题