Updated 'src/smart_pointers/rc-t'.

This commit is contained in:
Hector PENG
2026-04-04 10:28:27 +08:00
parent c5d0e25012
commit c97c752bcd
5 changed files with 97 additions and 75 deletions

View File

@@ -1,4 +1,3 @@
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
@@ -7,5 +6,7 @@ enum List {
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}

View File

@@ -10,8 +10,13 @@ use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println! ("创建 a 后的计数 = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
println! ("创建 b 后的计数 = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println! ("创建 c 后的计数 = {}", Rc::strong_count(&a));
}
println! ("b 为: {:?}\nc 为:{:?}", b, c);
println! ("c 超出作用域后的计数 = {}", Rc::strong_count(&a));
}

View File

@@ -102,7 +102,7 @@ Rust 之所以不允许我们显式地调用 `drop`,是因为 Rust 仍将在 `
我们无法禁用值超出作用域时 `drop` 的自动插入,并且无法显式调用 `drop` 方法。因此,当我们需要强制某个值被提前清理时,我们要使用 `std::mem::drop` 函数。
`std::mem::drop` 函数不同于 `Drop` 特质中的 `drop` 方法。我们通过作为参数传递我们打算强制弃用的值来调用他。这个函数位于前奏中,因此我们可以修改清单 15-15 中的 `main` 来调用 `drop` 函数,如下清单 15-16 中所示:
`std::mem::drop` 函数不同于 `Drop` 特质中的 `drop` 方法。我们通过作为参数传递我们打算强制弃用的值来调用他。这个函数前奏中,因此我们可以修改清单 15-15 中的 `main` 来调用 `drop` 函数,如下清单 15-16 中所示:
<a name="listing_15-16"></a>
文件名:`src/main.rs`

View File

@@ -1,31 +1,30 @@
# `Rc<T>`,引用计数的灵巧指针
在大多数情况下,所有权是明确的:咱们确切地知道哪个变量拥有给定值。然而,在有些情况下,单个值可能有多个所有者。例如,在图数据结构,多条边可能指向同一节点,而该节点在概念上所有指向他的边所拥有。一个节点不应该被清理,除非没有任何边指向他因此没有了所有者。
在大多数情况下,所有权是明确的:咱们确切地知道哪个变量拥有给定值。然而,在单个值可能有多个所有者的情形。例如,在图数据结构,多条边可能指向同一节点,进而这个节点在概念上所有指向他的边所拥有。节点不应该被清理,除非没有任何边指向他因此没有了所有者。
咱们必须使用 Rust 类型 `Rc<T>` 显式启用多重所有权,`Rc<T>`*引用计数reference counting* 的缩写。`Rc<T>` 类型记录了对一个值的引用数量,以确定该值是否仍在使用。如果对某个值的引用为零,那么这个值就可以被清理,而不会有任何引用变得无效。
咱们必须显式地通过使用 Rust 类型 `Rc<T>` 启用多重所有权,`Rc<T>`*引用计数reference counting* 的缩写。`Rc<T>` 类型会跟踪对值的引用数量,以确定该值是否仍在使用。当对值的引用为零时,则该值可被清理,而不会有没有任何引用变得无效。
请将 `Rc<T>` 设想为客厅里的一台电视。在有人进来看电视时,他们会打开。其他人可以进入客厅并观看电视。当最后一人离开客厅时,他们会关掉电视,因为他已不再被使用了。如果有人在其他人还在看电视的情况下关掉电视,剩下的电视的人就会哗然!
请将 `Rc<T>` 设想为客厅里的电视。当一个人进来看电视时,会打开电视。其他人可以进入客厅看电视。当最后一人离开客厅时,会关掉电视,因为电视不再被使用。当有人在其他人还在看电视关掉电视,剩下的电视观众就会哗然!
当我们想在内存堆上分配一数据程序的多个部分读取,且无法在编译时确定哪个部分最后使用完这些数据时,我们就使用 `Rc<T>` 类型。如果我们知道哪个部分会最后完成,我们就可以让那个部分成为数据的所有者,而在编译时执行的正常所有权规则就会生效。
当我们打算在堆上分配一数据程序的多个部分读取,且无法在编译时确定哪个部分最后结束使用该数据时,我们就使用 `Rc<T>` 类型。我们知道哪个部分会最后结束使用,就可以直接让那个部分成为数据的所有者,并且编译时执行的正常所有权规则就会生效。
请注意 `Rc<T>` 仅适用于单线程场景only for use in single-threaded scenarios。当们在第 16 章中讨论并发时,们将介绍如何在多线程程序中进行引用计数。
请注意`Rc<T>` 仅适用于单线程场景。当们在第 16 章中讨论并发时,们将介绍怎样在多线程程序中实现引用计数。
## 使用 `Rc<T>` 来共用数据
## 共用数据
**Using `Rc<T>` to Share Data**
让我们回到清单 15-5 中咱们的构造列表示例。回想一下,我们用 `Box<T>` 定义了他。这一次,我们将创建两个列表,二者会公用第三个列表的所有权。从概念上看,这类似于下图 15-3
我们来回到 [清单 15-5](./box-t.md#listing_15-5) 中的构造列表示例。回想一下,我们使用 `Box<T>` 定义了他。这一次,我们将创建两个列表,他们共用第三个列表的所有权。从概念上讲,这类似于下图 15-3。
<a name="f_15-3"></a>
![`b` 与 `c` 两个列表,共用了第三列表 `a` 的所有权](../images/15-03.svg)
*图 15-03两个列表`b` 与 `c`,共用第三个列表 `a` 的所有权*
**图 15-03**:两个列表`b``c`,共用第三个列表 `a` 的所有权
们将创建一个包含 `5``10` 的列表。然后我们再做两个列表:以 `3` 开头的 `b` 和以 `4` 开头的 `c`然后 `b``c` 列表都会延续到第一个包含 `5``10``a` 列表。换句话说,这两个列表将共用第一个包含 `5``10` 的列表。
们将创建包含 `5``10` 的列表 `a`。然后我们将构造两个列表:以 `3` 开头的 `b` 和以 `4` 开头的 `c`然后 `b``c` 两个列表都将接续到包含 `5``10`第一个列表 `a`。换句话说,这两个列表将共用包含 `5``10`第一个列表。
尝试使用带有 `Box<T>``List` 定义来实现这场景是行不通的,如示例 15-17 所示:
如清单 15-17 所示,尝试使用我们带有 `Box<T>``List` 定义来实现这场景是行不通的
<a name="listing_15-17"></a>
文件名:`src/main.rs`
```rust
@@ -43,98 +42,118 @@ fn main() {
}
```
*清单 15-17演示们不允许两个使用 `Box<T>` 的列表,试图共用第三个列表的所有权*
**清单 15-17**:演示们不允许两个使用 `Box<T>` 的列表,试图共用第三个列表的所有权
当我们编译这段代码时,我们得到了这样的报错:
当我们编译这段代码时,我们得到下面这个报错:
```console
$ cargo run
Compiling sp_demos v0.1.0 (/home/peng/rust-lang/sp_demos)
Compiling cons-list v0.1.0 (/home/hector/rust-lang-zh_CN/projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `sp_demos` due to previous error;
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
```
`Cons` 变种拥有他们持有的数据,所以当我们创建 `b` 列表时,`a` 被迁移到 `b``b` 拥有 `a`。然后,当我们在创建 `c` 时试图再次使用 `a`,我们不被允许,因为 `a`被迁移
`Cons` 变种拥有他们包含的数据,因此当我们创建列表 `b` 时,`a` 被迁移到 `b`进而 `b` 拥有 `a`。然后,当我们在创建 `c` 时试图再次使用 `a`,我们不被允许,因为 `a` 已被迁移。
们原本可以 `Cons` 的定义修改为持有引用,但那样咱们就必须指定生命周期参数。通过指定生命周期参数,咱们将指定列表中的每个元素,都至少整个列表的寿命一样长。清单 15-17 中的元素与列表就是这种情况,但并非在所有情况下都如此。
们原本可以修改 `Cons` 的定义为包含引用,但随后我们将必须指定生命周期参数。通过指定生命周期参数,我们实际上是在指定列表中的每个元素,都至少存活整个列表的寿命一样长。清单 15-17 中的元素与列表就是这种情况,但并非在所有情况下都如此。
相反,我们将改 `List` 的定义,使用 `Rc<T>` 来代替 `Box<T>`,如下清单 15-18 所示。现在每个 `Cons` 变种将持有一个值和一个指向 `List``Rc<T>`。当我们创建 `b` 时,我们将克隆 `a` 所持有`Rc<List>`,而不是取得 `a` 的所有权,从而将引用的数量从一增加到两个,并让 `a``b` 共用该 `Rc<List>` 中数据的所有权。在创建 `c` 时,我们也将克隆 `a`,将引用的数量从两个增加到三。每次我们调用 `Rc::clone``Rc<List>` 数据的引用数就会增加,除非对他的引用为零,否则数据不会被清理
相反,我们将`List` 的定义为在 `Box<T>` 处使用 `Rc<T>`,如下清单 15-18 所示。现在每个 `Cons` 变种将包含一个值和一个指向 `List``Rc<T>`。当我们创建 `b` 时,我们将克隆 `a` 包含`Rc<List>`,而不是取得 `a` 的所有权,从而将引用计数从一增加到,并让 `a``b` 共用该 `Rc<List>`数据的所有权。在创建 `c` 时,我们也将克隆 `a`,将引用计数从二增加到三。每次我们调用 `Rc::clone` 时,到 `Rc<List>` 数据的引用计数都将增加,并且除非对他的引用为零,否则数据不会被清理。
<a name="listing_15-18"></a>
文件名:`src/main.rs`
```rust
{{#include ../../projects/rc_demo/src/main.rs}}
```
enum List {
Cons(i32, Rc<List>),
Nil,
}
*清单 15-18使用 `Rc<T>` 的 `List` 定义*
use crate::List::{Cons, Nil};
use std::rc::Rc;
我们需要添加一个 `use` 语句来将 `Rc<T>` 引入作用域,因为他不在 Rust 前奏中。在 `main` 中,我们创建了包含 `5``10` 的列表,并将其存储在 `a` 中的新 `Rc<List>` 中。然后当我们创建 `b``c` 时,我们调用了 `Rc::clone` 函数,并将对 `a` 中的 `Rc<List>` 的引用作为参数传递。
我们本可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,但是 Rust 的惯例是在这种情况下使用 `Rc::clone``Rc::clone` 的实现并不像大多数类型的 `clone` 实现那样对所有数据进行深度拷贝。对 `Rc::clone` 的调用只是增加引用计数,这不会花费很多时间。数据的深度拷贝会花费很多时间。通过使用 `Rc::clone` 进行引用计数,我们可以直观地区分出深拷贝的那种克隆和增加引用计数的那种克隆。当寻找代码中的性能问题时,我们只需要考虑深拷贝的克隆,而可以不考虑对 `Rc::clone` 的调用。
> 注:第 4 章 [变量与数据交互方式之二:克隆](Ch04_Understanding_Ownership.md#变量与数据交互方式之二克隆) 中,曾提到:“当咱们看到对 `clone` 的调用时,咱们就知道一些任意的代码正在被执行,而这些代码可能开销很大。这是表明正在发生一些不同寻常事情的明显标志。”
## 克隆 `Rc<T>` 会增加引用计数
**Cloning an `Rc<T>` Increases the Reference Count**
我们来修改示例 15-18 中的工作示例,以便我们可以看到在我们创建和删除对 `a` 中的 `Rc<List>` 的引用时,引用计数会发生变化。
在下面清单 15-19 中,我们将更改 `main`,使其具有围绕列表 `c` 的内部作用域;然后我们可以看到当 `c` 超出作用域时引用计数如何变化。
文件名:`src/main.rs`
```rust
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println! ("在创建出 a 后,引用计数为 {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println! ("在创建出 b 后,引用计数为 {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println! ("在创建出 c 后,引用计数为 {}", Rc::strong_count(&a));
}
println! ("在 c 超出作用域后,引用计数为 {}", Rc::strong_count(&a));
let c = Cons(4, Rc::clone(&a));
}
```
*清单 15-19打印出引用计数*
**清单 15-18**:使用 `Rc<T>``List` 定义
在程序中引用计数发生变化的每一点上,我们都会打印引用计数,我们通过调用 `Rc::strong_count` 函数得到这个计数。这个函数被命名为 `strong_count` 而不是 `count`,是因为 `Rc<T>` 类型也有一个 `weak_count`;我们将在 [“防止引用循环:将 `Rc<T>` 变成 `Weak<T>`”](#防止引用循环将-rct-转变为-weakt) 小节中看到 `weak_count` 的用
我们需要添加一个 `use` 语句来带入 `Rc<T>` 到作用域,因为他不在前奏中。在 `main` 中,我们创建了包含 `5``10` 的列表并存储在 `a` 中的新 `Rc<List>` 中。然后,当我们创建 `b``c` 时,我们调用 `Rc::clone` 函数,并作为参数传递对 `a` 中的 `Rc<List>`用。
这段代码打印出以下内容:
我们本可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,但是 Rust 的约定是在这种情况下要使用 `Rc::clone``Rc::clone` 的实现并不像大多数类型的 `clone` 实现那样,会构造所有数据的深度拷贝。对 `Rc::clone` 的调用只会增加引用计数,这不会花费太多时间。数据的深度拷贝会花费很多时间。通过使用 `Rc::clone` 进行引用计数,我们可以直观地区分深拷贝的克隆类别和增加引用计数的克隆类别。在查找代码中的性能问题时,我们只需考虑深拷贝的克隆,而可以忽略对 `Rc::clone` 的调用。
> **译注**:第 4 章的 [变量与数据相互作用:克隆](../ownership/about_ownership.md#变量与数据相互作用克隆) 小节中,我们曾提到:“当咱们看到对 clone 的调用时,咱们就知道一些任意代码正被执行,而这些代码可能开销高昂。这是一些不同寻常事情正在发生的直观指示器。”
## 通过克隆增加引用计数
我们来修改示例 15-18 中的工作的示例,以便可以看到在创建和删除对 `a` 中的 `Rc<List>` 的引用时,引用计数的变化。
在下面清单 15-19 中,我们将更改 `main`,使其具有一个围绕列表 `c` 的内层作用域;然后,我们可以看到当 `c` 超出作用域时,引用计数如何变化。
<a name="listing_15-19"></a>
文件名:`src/main.rs`
```rust
-- --
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println! ("创建 a 后的计数 = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println! ("创建 b 后的计数 = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println! ("创建 c 后的计数 = {}", Rc::strong_count(&a));
}
println! ("c 超出作用域后的计数 = {}", Rc::strong_count(&a));
}
```
**清单 15-19**:打印引用计数
在程序中引用计数变化的每个点位,我们都打印引用计数,我们通过调用 `Rc::strong_count` 函数得到引用计数。这个函数之所以名为 `strong_count` 而不是 `count`,是因为 `Rc<T>` 类型还有个 `weak_count`;我们将在 [使用 `Weak<T>` 防止引用循环](./ref-cycles.md#使用-weakt-防止引用循环) 小节中看到 `weak_count` 的用途。
这段代码会打印以下内容:
```console
$ cargo run
Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos)
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/sp_demos`
创建 a 后,引用计数为 1
创建 b 后,引用计数为 2
创建 c 后,引用计数为 3
c 超出作用域后,引用计数为 2
Compiling rc_demo v0.1.0 (/home/hector/rust-lang-zh_CN/projects/rc_demo)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/rc_demo`
创建 a 后的计数 = 1
创建 b 后的计数 = 2
创建 c 后的计数 = 3
c 超出作用域后的计数 = 2
```
我们可以看到 `a` 中的 `Rc<List>` 的初始引用计数`1`;然后每次我们调用 `clone`,计数都会增加 `1`。当 `c` 超出作用域时,计数会减少 `1`。我们不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少引用计数: `Rc<T>` 值超出作用域时,`Drop` 特质的实现会自动减少引用计数。
我们可以看到`a` 中的 `Rc<List>` 有着 1 的初始引用计数;然后每次我们调用 `clone`,计数都会增加 1。当 `c` 超出作用域时,计数会减少 1。我们不必像必须调用 `Rc::clone` 增加引用计数那样调用函数来减少引用计数:`Drop` 特质的实现会在某个 `Rc<T>` 值超出作用域时,自动减少引用计数。
我们在这个例子中看不到的是,当 `b``a``main` 的末尾超出作用域时,计数为 `0`,并`Rc<List>` 会被完全清除。使用 `Rc<T>` 允许单个值有多个所有者,而计数确保只要任何所有者仍然存在,该值就保持有效。
我们在这个例子中看不到的是,当 `b``a``main` 结束处超出作用域时,计数为 0`Rc<List>` 会被彻底清理。使用 `Rc<T>` 允许单个值可以有多个所有者,而引用计数确保只要任何一个所有者仍然存在,该值就保持有效。
通过不可变引用,`Rc<T>` 允许咱们在程序的多个部分之间共用数据,仅供读取。如果 `Rc<T>` 允许咱们有多个可变引用,咱们可能会违反第章中讨论的一个借用规则:对同一个地方的多个可变借用,会导致数据竞和不一致。但是,能够修改数据是非常有用的!在下一节中,我们将讨论内部可变性模式和 `RefCell<T>` 类型,咱们可以将其`Rc<T>` 结合起来使用,以应对这种不可变性限制。
通过不可变引用,`Rc<T>` 允许咱们在程序的多个部分之间共用仅供读取的数据。若 `Rc<T>` 允许咱们有多个可变引用,咱们会违反第 4 章中讨论的借用规则之一:对同一的多个可变借用,会导致数据竞和不一致。但能够修改数据是非常有用的!在下一节中,我们将讨论内部可变性模式和 `RefCell<T>` 类型,咱们可以与 `Rc<T>` 结合使用该类型,以处理这一不可变性限制。
End

View File

@@ -70,10 +70,7 @@ b 的下一条目 = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
另一避免引用循环的办法是重新组织咱们的数据结构,使一些引用表达所有权,而一些引用不表达。由此,咱们可以有由一些所有权关系和一些非所有权关系组成的循环,而只有所有权关系会影响一个值是否可以被丢弃。在清单 15-25 中,我们总是希望 `Cons` 变体拥有他们的列表,所以重新组织数据结构是不可能的。咱们来看一个使用由父节点和子节点组成的图的示例,看看什么时候非所有权关系是防止引用循环的合适方式。
## 防止引用循环:将 `Rc<T>` 变为 `Weak<T>`
**Preventing Referencing Cycles: Turning an `Rc<T>` into `Weak<T>`**
## 使用 `Weak<T>` 防止引用循环
到目前为止,我们已经证明了调用 `Rc::clone` 会增加 `Rc<T>` 实例的 `strong_count`,而 `Rc<T>` 实例只有在其 `strong_count``0` 时才会被清理掉。咱们还可以通过调用 `Rc::downgrade` 并传递对 `Rc<T>` 的引用,来创建对 `Rc<T>` 实例中值的 *弱引用weak reference*。强引用是咱们共用 `Rc<T>` 实例所有权的方式。弱引用不表达所有权关系,他们的计数不会影响 `Rc<T>` 实例被清理的时间。他们不会引起引用循环,因为任何涉及弱引用的循环,都会在所涉及的值的强引用计数为 `0` 时被打破。