所有权回顾
本章介绍了许多新概念,如所有权、借用和 slice。如果您不熟悉系统编程,本章还介绍了内存分配、栈与堆、指针和未定义行为等新概念。在我们继续学习 Rust 的其余部分之前,让我们先停下来喘口气。我们将回顾和练习本章的关键概念。
所有权与垃圾回收
为了将所有权置于上下文中,我们应该谈谈垃圾回收。大多数编程语言使用垃圾回收器来管理内存,例如 Python、Javascript、Java 和 Go。垃圾回收器在运行时与正在运行的程序(至少是追踪回收器)相邻工作。回收器扫描内存以查找不再使用的数据——也就是说,运行的程序无法再从函数局部变量访问该数据。然后,回收器会释放未使用的内存以供以后使用。
垃圾回收的关键好处是它可以避免未定义行为(例如使用已释放的内存),这在 C 或 C++ 中可能会发生。垃圾回收还避免了需要复杂的类型系统来检查未定义行为,就像在 Rust 中一样。然而,垃圾回收也有一些缺点。一个明显的缺点是性能,因为垃圾回收会产生频繁的小开销(对于引用计数,如 Python 和 Swift)或不频繁的大开销(对于追踪,如所有其他 GC 语言)。
但另一个不太明显的缺点是,垃圾回收可能是不可预测的。为了说明这一点,假设我们正在实现一个 Document
类型,它表示一个可变的单词列表。我们可以用一种带有垃圾回收的语言(如 Python)以这种方式实现 Document
class Document:
def __init__(self, words: List[str]):
"""Create a new document"""
self.words = words
def add_word(self, word: str):
"""Add a word to the document"""
self.words.append(word)
def get_words(self) -> List[str]:
"""Get a list of all the words in the document"""
return self.words
以下是我们可以使用这个 Document
类的一种方式,它创建一个文档 d
,将其复制到一个新文档 d2
中,然后修改 d2
。
words = ["Hello"]
d = Document(words)
d2 = Document(d.get_words())
d2.add_word("world")
考虑关于这个示例的两个关键问题
-
words 数组何时被释放? 这个程序创建了三个指向同一数组的指针。变量
words
、d
和d2
都包含指向堆上分配的 words 数组的指针。因此,只有当所有三个变量都超出作用域时,Python 才会释放 words 数组。更一般地说,仅通过阅读源代码通常很难预测数据将在何处被垃圾回收。 -
文档
d
的内容是什么? 因为d2
包含指向与d
相同的 words 数组的指针,所以d2.add_word("world")
也会修改文档d
。因此,在本例中,d
中的单词是["Hello", "world"]
。发生这种情况是因为d.get_words()
返回对d
中 words 数组的可变引用。当数据结构可以泄漏其内部结构1时,普遍存在的隐式可变引用很容易导致不可预测的错误。在这里,对d2
的更改可以更改d
可能不是预期的行为。
这个问题并非 Python 独有——您可以在 C#、Java、Javascript 等中遇到类似的行为。事实上,大多数编程语言实际上都有指针的概念。这只是语言如何向程序员公开指针的问题。垃圾回收使得难以看清哪个变量指向哪个数据。例如,d.get_words()
生成指向 d
内数据的指针并不明显。
相比之下,Rust 的所有权模型将指针置于首要位置。我们可以通过将 Document
类型翻译成 Rust 数据结构来看到这一点。通常我们会使用 struct
,但我们还没有介绍过,所以我们只使用类型别名
#![allow(unused)] fn main() { type Document = Vec<String>; fn new_document(words: Vec<String>) -> Document { words } fn add_word(this: &mut Document, word: String) { this.push(word); } fn get_words(this: &Document) -> &[String] { this.as_slice() } }
这个 Rust API 在几个关键方面与 Python API 不同
-
函数
new_document
消耗输入向量words
的所有权。这意味着Document
拥有单词向量。当其拥有的Document
超出作用域时,单词向量将被可预测地释放。 -
函数
add_word
需要一个可变引用&mut Document
才能修改文档。它还消耗输入word
的所有权,这意味着没有人可以修改文档的单个单词。 -
函数
get_words
返回对文档中字符串的显式不可变引用。从这个单词向量创建新文档的唯一方法是深度复制其内容,如下所示
fn main() {
let words = vec!["hello".to_string()];
let d = new_document(words);
// .to_vec() converts &[String] to Vec<String> by cloning each string
let words_copy = get_words(&d).to_vec();
let mut d2 = new_document(words_copy);
add_word(&mut d2, "world".to_string());
// The modification to `d2` does not affect `d`
assert!(!get_words(&d).contains(&"world".into()));
}
这个例子的目的是说:如果 Rust 不是您的第一语言,那么您已经有使用内存和指针的经验了!Rust 只是使这些概念显式化。这具有双重好处:(1) 通过避免垃圾回收来提高运行时性能,以及 (2) 通过防止意外的“泄漏”数据来提高可预测性。
所有权的概念
接下来,让我们回顾一下所有权的概念。这个回顾将是快速的——目标是提醒您相关的概念。如果您意识到您忘记或不理解某个概念,那么我们将链接到您可以回顾的相关章节。
运行时的所有权
我们将首先回顾 Rust 如何在运行时使用内存
- Rust 在栈帧中分配局部变量,栈帧在函数被调用时分配,并在调用结束时释放。
- 局部变量可以保存数据(如数字、布尔值、元组等)或指针。
- 指针可以通过 box(拥有堆上数据的指针)或引用(非拥有指针)创建。
此图说明了每个概念在运行时的外观
回顾此图并确保您理解每个部分。例如,您应该能够回答
- 为什么
a_box_stack_ref
指向栈,而a_box_heap_ref
指向堆? - 为什么值
2
在 L2 时不再堆上? - 为什么
a_num
在 L2 时具有值5
?
如果您想回顾 box,请重新阅读 第 4.1 章。如果您想回顾引用,请重新阅读 第 4.2 章。如果您想查看涉及 box 和引用的案例研究,请重新阅读 第 4.3 章。
Slice 是一种特殊的引用,它引用内存中连续的数据序列。此图说明了 slice 如何引用字符串中字符的子序列
如果您想回顾 slice,请重新阅读 第 4.4 章。
编译时的所有权
Rust 跟踪每个变量的 R(读)、W(写)和 O(拥有)权限。Rust 要求变量具有适当的权限才能执行给定的操作。作为一个基本示例,如果一个变量未声明为 let mut
,则它缺少 W 权限,并且无法被修改
如果变量被移动或借用,则可以更改变量的权限。移动一个不可复制类型(如 Box<T>
或 String
)的变量需要 RO 权限,并且移动会消除变量上的所有权限。该规则阻止了已移动变量的使用
如果您想回顾移动的工作原理,请重新阅读 第 4.1 章。
借用变量(创建对其的引用)会暂时删除变量的一些权限。不可变借用创建不可变引用,并禁用被借用数据被修改或移动。例如,打印不可变引用是可以的
但是修改不可变引用是不可以的
修改不可变借用的数据是不可以的
并且从引用中移出数据是不可以的
可变借用创建可变引用,这会禁用被借用数据被读取、写入或移动。例如,修改可变引用是可以的
但是访问可变借用的数据是不可以的
如果您想回顾权限和引用,请重新阅读 第 4.2 章。
连接编译时和运行时的所有权
Rust 的权限旨在防止未定义行为。例如,一种未定义行为是释放后使用,即读取或写入已释放的内存。不可变借用删除 W 权限以避免释放后使用,就像在这种情况下一样
另一种未定义行为是双重释放,即内存被释放两次。对不可复制数据的引用的解引用没有 O 权限来避免双重释放,就像在这种情况下一样
如果您想回顾未定义行为,请重新阅读 第 4.1 章 和 第 4.3 章。
所有权的其余部分
当我们介绍诸如结构体、枚举和 trait 等附加功能时,这些功能将与所有权进行特定的交互。本章为理解这些交互提供了必要的基础——内存、指针、未定义行为和权限的概念将帮助我们在以后的章节中讨论 Rust 更高级的部分。
如果您想检查您的理解程度,请不要忘记参加测验!
事实上,所有权类型的最初发明根本不是为了内存安全。它是为了防止在类似 Java 的语言中泄漏对数据结构内部的可变引用。如果您有兴趣了解更多关于所有权类型的历史,请查看论文 “Ownership Types for Flexible Alias Protection” (Clarke et al. 1998)。