trpl-zh-cn/src/ch15-06-reference-cycles.md

165 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 引用循环与内存泄漏
> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/main/src/ch15-06-reference-cycles.md) <br>
> commit bd27a8b72336610c9a200f0ca932ffc8b6fb5ee1
Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄漏**_memory leak_但并不是不可能。与在编译时拒绝数据竞争不同 Rust 并不保证完全地避免内存泄漏,这意味着内存泄漏在 Rust 被认为是内存安全的。这一点可以通过 `Rc<T>``RefCell<T>` 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0其值也永远不会被丢弃。
### 制造引用循环
让我们看看引用循环是如何发生的以及如何避免它。以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始:
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-25/src/main.rs}}
```
<span class="caption">示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据</span>
这里采用了示例 15-25 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell<Rc<List>>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。
在示例 15-26 中增加了一个 `main` 函数,其使用了示例 15-25 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `a` 中的列表指向 `b` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。
<span class="filename">文件: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-26/src/main.rs:here}}
```
<span class="caption">示例 15-26创建一个引用循环两个 `List` 值互相指向彼此</span>
这里在变量 `a` 中创建了一个 `Rc<List>` 实例来存放初值为 `5, Nil``List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a``List` 的另一个 `Rc<List>` 实例。
最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a``RefCell<Rc<List>>` 的引用,并放入变量 `link` 中。接着使用 `RefCell<Rc<List>>``borrow_mut` 方法将其值从存放 `Nil``Rc<List>` 修改为 `b` 中的 `Rc<List>`
如果保持最后的 `println!` 行注释并运行代码,会得到如下输出:
```console
{{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}}
```
可以看到将列表 `a` 修改为指向 `b` 之后, `a``b` 中的 `Rc<List>` 实例的引用计数都是 2。在 `main` 的结尾Rust 丢弃 `b`,这会 `b` `Rc<List>` 实例的引用计数从 2 减为 1。然而`b` `Rc<List>` 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 `a``a` `Rc<List>` 实例的引用计数从 2 减为 1。这个实例也不能被回收因为 `b` `Rc<List>` 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更形象的展示我们创建了一个如图 15-4 所示的引用循环:
<img alt="Reference cycle of lists" src="img/trpl15-04.svg" class="center" />
<span class="caption">图 15-4: 列表 `a``b` 彼此互相指向形成引用循环</span>
如果取消最后 `println!` 的注释并运行程序Rust 会尝试打印出 `a` 指向 `b` 指向 `a` 这样的循环直到栈溢出。
这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。
创建引用循环并不容易,但也不是不可能。如果你有包含 `Rc<T>``RefCell<T>` 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。
另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。
### 避免引用循环:将 `Rc<T>` 变为 `Weak<T>`
到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc<T>` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时才会被清理的 `Rc<T>` 实例。你也可以通过调用 `Rc::downgrade` 并传递 `Rc<T>` 实例的引用来创建其值的 **弱引用**_weak reference_。调用 `Rc::downgrade` 时会得到 `Weak<T>` 类型的智能指针。不同于将 `Rc<T>` 实例的 `strong_count` 加 1调用 `Rc::downgrade` 会将 `weak_count` 加 1。`Rc<T>` 类型使用 `weak_count` 来记录其存在多少个 `Weak<T>` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc<T>` 实例被清理。
强引用代表如何共享 `Rc<T>` 实例的所有权,但弱引用并不属于所有权关系。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
因为 `Weak<T>` 引用的值可能已经被丢弃了,为了使用 `Weak<T>` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak<T>` 实例的 `upgrade` 方法,这会返回 `Option<Rc<T>>`。如果 `Rc<T>` 值还未被丢弃,则结果是 `Some`;如果 `Rc<T>` 已被丢弃,则结果是 `None`。因为 `upgrade` 返回一个 `Option<Rc<T>>`Rust 会确保处理 `Some``None` 的情况,所以它不会返回非法指针。
我们会创建一个某项知道其子项和父项的树形结构的例子,而不是只知道其下一项的列表。
#### 创建树形数据结构:带有子节点的 `Node`
在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 `i32` 值和其子节点引用的 `Node`
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:here}}
```
我们希望能够 `Node` 拥有其子节点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个 `Node`,为此 `Vec<T>` 的项的类型被定义为 `Rc<Node>`。我们还希望能修改其他节点的子节点,所以 `children``Vec<Rc<Node>>` 被放进了 `RefCell<T>`
接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子节点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子节点的实例 `branch`,如示例 15-27 所示:
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:there}}
```
<span class="caption">示例 15-27创建没有子节点的 `leaf` 节点和以 `leaf` 作为子节点的 `branch` 节点</span>
这里克隆了 `leaf` 中的 `Rc<Node>` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children``branch` 中获得 `leaf`,不过无法从 `leaf``branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父节点。稍后我们会这么做。
#### 增加从子到父的引用
为了使子节点知道其父节点,需要在 `Node` 结构体定义中增加一个 `parent` 字段。问题是 `parent` 的类型应该是什么。我们知道其不能包含 `Rc<T>`,因为这样 `leaf.parent` 将会指向 `branch``branch.children` 会包含 `leaf` 的指针,这会形成引用循环,会造成其 `strong_count` 永远也不会为 0。
现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
所以 `parent` 使用 `Weak<T>` 类型而不是 `Rc<T>`,具体来说是 `RefCell<Weak<Node>>`。现在 `Node` 结构体定义看起来像这样:
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:here}}
```
这样,一个节点就能够引用其父节点,但不拥有其父节点。在示例 15-28 中,我们更新 `main` 来使用新定义以便 `leaf` 节点可以通过 `branch` 引用其父节点:
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:there}}
```
<span class="caption">示例 15-28一个 `leaf` 节点,其拥有指向其父节点 `branch``Weak` 引用</span>
创建 `leaf` 节点类似于示例 15-27 中如何创建 `leaf` 节点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父节点,所以我们新建了一个空的 `Weak` 引用实例。
此时,当尝试使用 `upgrade` 方法获取 `leaf` 的父节点引用时,会得到一个 `None` 值。如第一个 `println!` 输出所示:
```text
leaf parent = None
```
当创建 `branch` 节点时,其也会新建一个 `Weak<Node>` 引用,因为 `branch` 并没有父节点。`leaf` 仍然作为 `branch` 的一个子节点。一旦在 `branch` 中有了 `Node` 实例,就可以修改 `leaf` 使其拥有指向父节点的 `Weak<Node>` 引用。这里使用了 `leaf``parent` 字段里的 `RefCell<Weak<Node>>``borrow_mut` 方法,接着使用了 `Rc::downgrade` 函数来从 `branch` 中的 `Rc<Node>` 值创建了一个指向 `branch``Weak<Node>` 引用。
当再次打印出 `leaf` 的父节点时,这一次将会得到存放了 `branch``Some` 值:现在 `leaf` 可以访问其父节点了!当打印出 `leaf` 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:`Weak<Node>` 引用被打印为 `(Weak)`
```text
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
```
没有无限的输出表明这段代码并没有造成引用循环。这一点也可以从观察 `Rc::strong_count``Rc::weak_count` 调用的结果看出。
#### 可视化 `strong_count` 和 `weak_count` 的改变
让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc<Node>` 实例的 `strong_count``weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-29 所示:
<span class="filename">文件名: src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-29/src/main.rs:here}}
```
<span class="caption">示例 15-29在内部作用域创建 `branch` 并检查其强弱引用计数</span>
一旦创建了 `leaf`,其 `Rc<Node>` 的强引用计数为 1弱引用计数为 0。在内部作用域中创建了 `branch` 并与 `leaf` 相关联,此时 `branch``Rc<Node>` 的强引用计数为 1弱引用计数为 1因为 `leaf.parent` 通过 `Weak<Node>` 指向 `branch`)。这里 `leaf` 的强引用计数为 2因为现在 `branch``branch.children` 中储存了 `leaf``Rc<Node>` 的拷贝,不过弱引用计数仍然为 0。
当内部作用域结束时,`branch` 离开作用域,`Rc<Node>` 的强引用计数减少为 0所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄漏!
如果在内部作用域结束后尝试访问 `leaf` 的父节点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc<Node>` 的强引用计数为 1弱引用计数为 0因为现在 `leaf` 又是 `Rc<Node>` 唯一的引用了。
所有这些管理计数和值的逻辑都内建于 `Rc<T>``Weak<T>` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子节点到父节点的关系为一个`Weak<T>`引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。
## 总结
这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。`Box<T>` 有一个已知的大小并指向分配在堆上的数据。`Rc<T>` 记录了堆上数据的引用数量以便可以拥有多个所有者。`RefCell<T>` 和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。
我们还介绍了提供了很多智能指针功能的 trait `Deref``Drop`。同时探索了会造成内存泄漏的引用循环,以及如何使用 `Weak<T>` 来避免它们。
如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 [“The Rustonomicon”][nomicon] 来获取更多有用的信息。
接下来,让我们谈谈 Rust 的并发。届时甚至还会学习到一些新的对并发有帮助的智能指针。
[nomicon]: https://doc.rust-lang.org/nomicon/index.html