Merge pull request #426 from wtklbm/master

第二次更新智能指针部分内容
This commit is contained in:
KaiserY 2020-06-27 21:51:19 +08:00 committed by GitHub
commit b5de0af973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 26 additions and 26 deletions

View File

@ -4,8 +4,8 @@
> commit 57adb83f69a69e20862d9e107b2a8bab95169b4c
对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box<T>` 自定义了 `Drop` 用来释放 box 所指向的堆空间。
在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄露资源。
不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源。
在其他一些语言中,我们
指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`

View File

@ -3,7 +3,7 @@
> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/src/ch15-04-rc.md) > <br>
> commit 6f292c8439927b4c5b870dd4afd2bfc52cc4eccc
大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。结点直到没有任何边指向它之前都不应该被清理。
大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理。
为了启用多所有权Rust 有一个叫做 `Rc<T>` 的类型。其名称为 **引用计数**_reference counting_的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用就代表没有任何有效引用并可以被清理。
@ -65,7 +65,7 @@ error[E0382]: use of moved value: `a`
可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。例如,借用检查器不会允许 `let a = Cons(10, &Nil);` 编译,因为临时值 `Nil` 会在 `a` 获取其引用之前就被丢弃了。
相反,我们修改 `List` 的定义为使用 `Rc<T>` 代替 `Box<T>`,如列表 15-18 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List``Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a``b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone``Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
相反,我们修改 `List` 的定义为使用 `Rc<T>` 代替 `Box<T>`,如列表 15-18 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List``Rc<T>`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc<List>`,这会将引用计数从 1 增加到 2 并允许 `a``b` 共享 `Rc<List>` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone``Rc<List>` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
<span class="filename">文件名: src/main.rs</span>
@ -136,7 +136,7 @@ count after c goes out of scope = 2
我们能够看到 `a``Rc<List>` 的初始引用计数为1接着每次调用 `clone`计数会增加1。当 `c` 离开作用域时计数减1。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc<T>` 值离开作用域时自动减少引用计数。
从这个例子我们所不能看到的是,在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0同时 `Rc` 被完全清理。使用 `Rc` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。
从这个例子我们所不能看到的是,在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0同时 `Rc<List>` 被完全清理。使用 `Rc<T>` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。
通过不可变引用, `Rc<T>` 允许在程序的多个部分之间只读地共享数据。如果 `Rc<T>` 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 `RefCell<T>` 类型,它可以与 `Rc<T>` 结合使用来处理不可变性的限制。

View File

@ -89,7 +89,7 @@ fn main() {
这里在变量 `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` 修改为 `b` 中的 `Rc<List>`
最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a``RefCell<Rc<List>>` 的引用,并放入变量 `link` 中。接着使用 `RefCell<Rc<List>>``borrow_mut` 方法将其值从存放 `Nil``Rc<List>` 修改为 `b` 中的 `Rc<List>`
如果保持最后的 `println!` 行注释并运行代码,会得到如下输出:
@ -103,7 +103,7 @@ b rc count after changing a = 2
a rc count after changing a = 2
```
可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc<List>` 实例的引用计数为 2。在 `main` 的结尾Rust 会尝试首先丢弃 `b`,这会使 `a``b``Rc` 实例的引用计数减 1。
可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc<List>` 实例的引用计数为 2。在 `main` 的结尾Rust 会尝试首先丢弃 `b`,这会使 `a``b``Rc<List>` 实例的引用计数减 1。
然而,因为 `a` 仍然引用 `b` 中的 `Rc<List>``Rc<List>` 的引用计数是 1 而不是 0所以 `Rc<List>` 在堆上的内存不会被丢弃。其内存会因为引用计数为 1 而永远停留。为了更形象的展示,我们创建了一个如图 15-4 所示的引用循环:
@ -117,11 +117,11 @@ a rc count after changing a = 2
创建引用循环并不容易,但也不是不可能。如果你有包含 `Rc<T>``RefCell<T>` 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。
另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父结点和子结点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。
另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。
### 避免引用循环:将 `Rc<T>` 变为 `Weak<T>`
到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc<T>` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时才会被清理的 `Rc<T>` 实例。你也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**_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` 实例被清理。
到目前为止,我们已经展示了调用 `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 时被打断。
@ -129,7 +129,7 @@ a rc count after changing a = 2
我们会创建一个某项知道其子项**和**父项的树形结构的例子,而不是只知道其下一项的列表。
#### 创建树形数据结构:带有子点的 `Node`
#### 创建树形数据结构:带有子点的 `Node`
在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 `i32` 值和其子节点引用的 `Node`
@ -146,9 +146,9 @@ struct Node {
}
```
我们希望能够 `Node` 拥有其子点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个 `Node`,为此 `Vec<T>` 的项的类型被定义为 `Rc<Node>`。我们还希望能修改其他结点的子结点,所以 `children``Vec<Rc<Node>>` 被放进了 `RefCell<T>`
我们希望能够 `Node` 拥有其子点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个 `Node`,为此 `Vec<T>` 的项的类型被定义为 `Rc<Node>`。我们还希望能修改其他节点的子节点,所以 `children``Vec<Rc<Node>>` 被放进了 `RefCell<T>`
接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子结点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子结点的实例 `branch`,如示例 15-27 所示:
接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子节点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子节点的实例 `branch`,如示例 15-27 所示:
<span class="filename">文件名: src/main.rs</span>
@ -175,15 +175,15 @@ fn main() {
}
```
<span class="caption">示例 15-27创建没有子结点的 `leaf` 结点和以 `leaf` 作为子结点的 `branch`</span>
<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` 是其父点。稍后我们会这么做。
这里克隆了 `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.
为了使子节点知道其父节点,需要在 `Node` 结构体定义中增加一个 `parent` 字段。问题是 `parent` 的类型应该是什么。我们知道其不能包含 `Rc<T>`,因为这样 `leaf.parent` 将会指向 `branch``branch.children` 会包含 `leaf` 的指针,这会形成引用循环,会造成其 `strong_count` 永远也不会为 0.
现在换一种方式思考这个关系,父结点应该拥有其子结点:如果父结点被丢弃了,其子结点也应该被丢弃。然而子结点不应该拥有其父结点:如果丢弃子结点,其父结点应该依然存在。这正是弱引用的例子!
现在换一种方式思考这个关系,父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。这正是弱引用的例子!
所以 `parent` 使用 `Weak<T>` 类型而不是 `Rc<T>`,具体来说是 `RefCell<Weak<Node>>`。现在 `Node` 结构体定义看起来像这样:
@ -201,7 +201,7 @@ struct Node {
}
```
这样,一个结点就能够引用其父结点,但不拥有其父结点。在示例 15-28 中,我们更新 `main` 来使用新定义以便 `leaf` 结点可以通过 `branch` 引用其父结点:
这样,一个节点就能够引用其父节点,但不拥有其父节点。在示例 15-28 中,我们更新 `main` 来使用新定义以便 `leaf` 节点可以通过 `branch` 引用其父节点:
<span class="filename">文件名: src/main.rs</span>
@ -237,19 +237,19 @@ fn main() {
}
```
<span class="caption">示例 15-28一个 `leaf` 结点,其拥有指向其父结`branch``Weak` 引用</span>
<span class="caption">示例 15-28一个 `leaf` 节点,其拥有指向其父节`branch``Weak` 引用</span>
创建 `leaf` 结点类似于示例 15-27 中如何创建 `leaf` 结点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父结点,所以我们新建了一个空的 `Weak` 引用实例。
创建 `leaf` 节点类似于示例 15-27 中如何创建 `leaf` 节点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父节点,所以我们新建了一个空的 `Weak` 引用实例。
此时,当尝试使用 `upgrade` 方法获取 `leaf` 的父点引用时,会得到一个 `None` 值。如第一个 `println!` 输出所示:
此时,当尝试使用 `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` 值创建了一个指向 `branch``Weak<Node>` 引用。
当创建 `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)`
当再次打印出 `leaf` 的父点时,这一次将会得到存放了 `branch``Some` 值:现在 `leaf` 可以访问其父点了!当打印出 `leaf` 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:`Weak<Node>` 引用被打印为 `(Weak)`
```text
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
@ -324,17 +324,17 @@ fn main() {
一旦创建了 `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` 是否被丢弃无关,所以并没有产生任何内存泄
当内部作用域结束时,`branch` 离开作用域,`Rc<Node>` 的强引用计数减少为 0所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄
如果在内部作用域结束后尝试访问 `leaf` 的父点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc<Node>` 的强引用计数为 1弱引用计数为 0因为现在 `leaf` 又是 `Rc<Node>` 唯一的引用了。
如果在内部作用域结束后尝试访问 `leaf` 的父点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc<Node>` 的强引用计数为 1弱引用计数为 0因为现在 `leaf` 又是 `Rc<Node>` 唯一的引用了。
所有这些管理计数和值的逻辑都内建于 `Rc<T>``Weak<T>` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子结点到父结点的关系为一个`Weak<T>`引用,就能够拥有父结点和子结点之间的双向引用而不会造成引用循环和内存泄露
所有这些管理计数和值的逻辑都内建于 `Rc<T>``Weak<T>` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子节点到父节点的关系为一个`Weak<T>`引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏
## 总结
这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。`Box<T>` 有一个已知的大小并指向分配在堆上的数据。`Rc<T>` 记录了堆上数据的引用数量以便可以拥有多个所有者。`RefCell<T>` 和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。
我们还介绍了提供了很多智能指针功能的 trait `Deref``Drop`。同时探索了会造成内存泄的引用循环,以及如何使用 `Weak<T>` 来避免它们。
我们还介绍了提供了很多智能指针功能的 trait `Deref``Drop`。同时探索了会造成内存泄的引用循环,以及如何使用 `Weak<T>` 来避免它们。
如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 [“The Rustonomicon”][nomicon] 来获取更多有用的信息。