Rust 中的所有权

11 min read

所有权是 Rust 中最重要的概念之一,也是 Rust 与 C++、Java 等“别的语言”最大的区别之一。

规定所有权规则是为了解决一个困扰了学界多年的难题:如何高效的管理内存?

每一个运行的程序都必须管理它使用的内存。有的语言让程序猿自己管理程序使用的内存 (比如:C 语言)。有的语言通过垃圾回收 (Garbage Collection, GC) 来自动的管理内存 (比如:Java)。它们各自有各自的有点,同时又有各自的缺点——GC 可以自动回收程序不需要的内存,可以降低程序员的心智负担,但运行 GC 会暂停程序的运行,虽然时间很短,但对于某些对时间特别敏感的程序 (比如交易股票、期货的程序) 来说是不可接受的。那么手动管理内存呢?确实,手动管理内存不会暂停程序的执行,但是如果忘记回收了会浪费内存;如果过早回收了,将会出现无效变量;如果重复回收,这也是个 bug;这无疑会对程序的运行带来不确定性。所有权规则就是为了让程序不暂停同时让内存被正确释放的而制定的。

先说结论:

  • Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量。
  • 值有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。

那么究竟谁才是程序的所有者呢?

让我们举个栗子:

fn main () {
    let s1 = "hello"; // 从这里开始 s1 是有效的,但 s2 是无效的
 {
     let s2 = String::from("world"); // 从这里开始 s1 和 s2 都是有效的
 } // s2 离开了它的作用域,被丢弃;但 s1 仍然是有效的
} // s1 离开了它的作用域,被丢弃;程序结束

s1 来说,整个 main 函数都是它的作用域,它的所有者就是 main。而 s2 的作用域在 {} 之间,所以它的所有者是 {}。也就是说如果在 {} 后使用 s1 是合法的,但使用 s2 就会报错,因为 s2 离开了它的作用域,已经被丢弃了。

好像挺好理解的,那么它又是如何影响程序对内存的管理的呢?

内存与分配

首先我们要了解以下程序是如何使用内存的。

Rust 对内存有两种使用方式:栈 (Stack)堆 (Heap)

栈的特点是先进后出,也就是说先入栈的数据会最后出栈。并且栈中数据的大小的确定的。这些特点使得栈的操作速度非常的快。所以栈被用来存放整型、浮点型等长度固定的数据类型。

访问堆上的数据就要比访问栈上的数据要慢上不少。因为堆上的数据大小是不固定的,并且必须通过指针来访问。现代处理器在内存 (缓存) 中跳转越少就越快。因此堆一般用来存放那些大小未知或者大小可能发生变化的数据类型。

所有权的交互方式

移动

让我们来看看这么一个栗子:

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

按照正常是思路程序应该会打印出 hello, world!。因为我们让 s2 拷贝了 s1 的指针,就像这样:

{{RAM}}

但事实上程序会报错!因为 s1 中的指针移动到了 s2 中而并非拷贝,就像这样:

{{RAM}}

在解释为什么之前先让我们来假设一下 s1 中的指针被拷贝到了 s2 中。那么就有两个指针同时指向了 String

{{RAM}}

然后在离开作用域时,s1s2 指向的对象 (也就是 String) 被依次释放掉。注意到了吗?String 被释放了两次!这是一个叫做 二次释放 (double free) 的错误,也是之前提到过的内存安全性 bug 之一。两次释放 (相同) 内存会导致内存污染,它可能会导致潜在的安全漏洞。

为了避免这一错误,Rust 好在将 s1 中的指针赋值给 s2 的同时抛弃掉 s1

{{RAM}}

这也印证了**“值有且只有一个所有者”**这一规则。

克隆

如果想在将 s1 中的指针赋值给 s2 后仍然使用 s1,可以使用 clone 方法。

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

可以正常运行,堆上的数据确实被复制了,但所有权并没有发生移动。此时的内存大概是这个样子:

{{RAM}}

拷贝

让我们来执行一下以下代码:

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

这段代码能够运行,并且没有使用 clone 方法。这似乎和我们刚学到的有点矛盾。

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。所以 x 的值直接被复制给了 y,相当于默认使用了 clone 方法。

相似的数据类型还有:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。

所有权与函数

与赋值类似,函数的参数与返回值也可能发生所有权的转移。

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到 takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

使用值但不获得所有权

引用允许你使用值但不获取其所有权

我们将获取引用作为函数参数称为借用 (borrowing)

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 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
  // 所以什么也不会发生

详情:The Rust Programming Language::References and Borrowing

结束了?不!仅仅是开始…

Rust 中关于所有权还有很多重要的概念,比如生命周期 (lifetime) 等…

所以我才说 Rust 中的所有权既是重点,也是难点。

受限于篇幅,就不一一介绍了,更多细节可以参考:

相关文章



说明

这篇文章是在学习完 The Rust Programming Language::What Is Ownership? 后写的,目的是巩固知识

看懂了不一定是真懂了,讲出来让别人听懂才是真懂了

内容可能与原教材原文有雷同的地方。


顺便安利一下这本书:The Rust Programming Language

这是 Rust 的官方教材,写得十分详细。现在已经很难买到这样的书 (更何况这还是免费的)

如果觉得英语啃得恼火也可以看 KaiserY 大佬翻译的版本:Rust 程序设计语言 简体中文版

Previous article
Next article