diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index cb11eb4..3251288 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -1,65 +1,25 @@ # 智能指针 -> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-00-smart-pointers.md) +> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/src/ch15-00-smart-pointers.md) >
-> commit 68267b982a226fa252e9afa1a5029396ccf5fa03 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据它们没有任何其他特殊功能。它们也没有任何额外开销,所以应用的最多。 +**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据没有任何其他特殊功能。它们也没有任何额外开销,所以应用的最多。 另一方面,**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中不同的智能指针提供了多于引用的额外功能。本章将会探索的一个例子便是 **引用计数** (*reference counting*)智能指针类型,其允许数据有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。 - - - - - - 在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 **拥有** 他们指向的数据。 实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。 - - - 智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。 考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些: - - - * `Box`,用于在堆上分配值 * `Rc`,一个引用计数类型,其数据可以有多个所有者 * `Ref` 和 `RefMut`,通过 `RefCell` 访问,一个在运行时而不是在编译时执行借用规则的类型。 - - - -同时我们会涉及 **内部可变性**(*interior mutability*)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 **引用循环**(*reference cycles*)会如何泄露内存,以及如何避免。 +另外我们会涉及 **内部可变性**(*interior mutability*)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 **引用循环**(*reference cycles*)会如何泄露内存,以及如何避免。 让我们开始吧! diff --git a/src/ch15-01-box.md b/src/ch15-01-box.md index 0c5703d..272bf89 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -1,23 +1,18 @@ ## `Box` 在堆上存储数据,并且可确定大小 -> [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-01-box.md) +> [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/src/ch15-01-box.md) >
-> commit 0905e41f7387b60865e6eac744e31a7f7b46edf5 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 最简单直接的智能指针是 *box*,其类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 - - - -除了数据被储存在堆上而不是栈上之外,box 没有性能损失,不过也没有很多额外的功能。他们多用于如下场景: +除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景: - 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 - 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 -我们将在本部分的余下内容中展示第一种应用场景。作为对另外两个情况更详细的说明:在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十七章刚好有一整个部分专门讲解这个主题。所以这里所学的内容会在第十七章再次用上! +我们会在 “box 允许创建递归类型” 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十七章刚好有一整个部分 “为使用不同类型的值而设计的 trait 对象” 专门讲解这个主题。所以这里所学的内容会在第十七章再次用上! ### 使用 `Box` 在堆上储存数据 @@ -40,62 +35,27 @@ fn main() { 将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。将像单个 `i32` 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用 box 时无法定义的类型的例子。 - - - ### box 允许创建递归类型 - - - - - Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 **递归类型**(*recursive type*),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。 让我们探索一下 *cons list*,一个函数式编程语言中的常见类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 - - - -cons list 是一个每一项都包含两个部分的列表:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值并没有下一项。 - -> #### cons list 的更多内容 -> -> *cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons` 函数(“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。 -> -> cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接” 通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。 -> -> cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 +#### cons list 的更多内容 -注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 +*cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons` 函数(“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。 + +cons 函数的概念涉及到更通用的函数式编程术语;“将 *x* 与 *y* 连接” 通常意味着构建一个新的容器而将 *x* 的元素放在新容器的开头,其后则是容器 *y* 的元素。 - - +cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值并没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。 + +注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 示例 15-2 包含一个 cons list 的枚举定义。注意这还不能编译因为这个类型没有已知的大小,之后我们会展示: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile enum List { Cons(i32, List), Nil, @@ -104,13 +64,7 @@ enum List { 示例 15-2:第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举 -> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型实现它,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 - - - +> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 使用这个 cons list 来储存列表 `1, 2, 3` 将看起来如示例 15-3 所示: @@ -128,11 +82,11 @@ fn main() { 第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2` 的 `Cons` 值和下一个 `List` 值。接着又有另一个存放了 `3` 的 `Cons` 值和最后一个值为 `Nil` 的 `List`,非递归成员代表了列表的结尾。 -如果尝试编译上面的代码,会得到如示例 15-4 所示的错误: +如果尝试编译示例 15-3 的代码,会得到如示例 15-4 所示的错误: ```text error[E0072]: recursive type `List` has infinite size - --> + --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ recursive type has infinite size @@ -145,14 +99,6 @@ error[E0072]: recursive type `List` has infinite size 示例 15-4:尝试定义一个递归枚举时得到的错误 - - - 这个错误表明这个类型 “有无限的大小”。其原因是 `List` 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点来看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 ### 计算非递归类型的大小 @@ -170,28 +116,26 @@ enum Message { 当 Rust 需要知道要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因此,`Message` 值所需的空间等于储存其最大成员的空间大小。 -与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-5 所示: +与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-1 所示: An infinite Cons list -图 15-5:一个包含无限个 `Cons` 成员的无限 `List` +图 15-1:一个包含无限个 `Cons` 成员的无限 `List` ### 使用 `Box` 给递归类型一个已知的大小 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了示例 15-4 中的错误。这个错误也包括了有用的建议: ```text -= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to - make `List` representable + = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to + make `List` representable ``` 在建议中,“indirection” 意味着不同于直接储存一个值,我们将间接的储存一个指向值的指针。 -因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。 - -所以可以将 `Box` 放入 `Cons` 成员中而不是直接存放另一个 `List` 值。`Box` 会指向另一个位于堆上的 `List` 值,而不是存放在 `Cons` 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 +因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 `Box` 放入 `Cons` 成员中而不是直接存放另一个 `List` 值。`Box` 会指向另一个位于堆上的 `List` 值,而不是存放在 `Cons` 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 -我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-6 所示,这是可以编译的: +我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-65 所示,这是可以编译的: 文件名: src/main.rs @@ -211,21 +155,14 @@ fn main() { } ``` -示例 15-6:为了拥有已知大小而使用 `Box` 的 `List` 定义 +示例 15-5:为了拥有已知大小而使用 `Box` 的 `List` 定义 -`Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-7 展示了现在 `Cons` 成员看起来像什么: +`Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-2 展示了现在 `Cons` 成员看起来像什么: A finite Cons list -图 15-7:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 +图 15-2:因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了 -box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。他们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。 +box 只提供了间接存储和堆分配;他们并没有任何其他特殊的功能,比如我们将会见到的其他智能指针。它们也没有这些特殊功能带来的性能损失,所以他们可以用于像 cons list 这样间接存储是唯一所需功能的场景。我们还将在第十七章看到 box 的更多应用场景。 `Box` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box` 值被当作引用对待。当 `Box` 值离开作用域时,由于 `Box` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。让我们更详细的探索一下这两个 trait;这些 trait 在本章余下讨论的其他智能指针所提供的功能中将会更为重要。 - - - diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 27aecf0..ef79c4a 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -1,49 +1,18 @@ ## 通过 `Deref` trait 将智能指针当作常规引用处理 -> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md) +> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/src/ch15-02-deref.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -实现 `Deref` trait 允许我们重载 **解引用运算符**(*dereference operator*)`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 +实现 `Deref` trait 允许我们重载 **解引用运算符**(*dereference operator*)`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 - - +让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态**(*deref coercions*)功能和它是如何一同处理引用或智能指针的。 - - +> 我们将要构建的 `MyBox` 类型与真正的 `Box` 有一个巨大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 Deref`,所以其数据实际存放在何处相比其类似指针的行为来说不算重要。 -让我们首先看看 `*` 如何处理引用,接着尝试定义我们自己的类 `Box` 类型并看看为何 `*` 不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态**(*deref coercions*)功能和它是如何一同处理引用或智能指针的。 +### 通过解引用运算符追踪指针的值 -### 通过 `*` 追踪指针的值 - - - - - - - -常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-8 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据: - - - - - - +常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-6 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据: 文件名: src/main.rs @@ -57,7 +26,7 @@ fn main() { } ``` -示例 15-8:使用解引用运算符来跟踪 `i32` 值的引用 +示例 15-6:使用解引用运算符来跟踪 `i32` 值的引用 变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 @@ -75,11 +44,11 @@ not satisfied `{integer}` ``` -不允许比较数字的引用与数字,因为它们是不同的类型。必须使用 `*` 追踪引用所指向的值。 +不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符追踪引用所指向的值。 ### 像引用一样使用 `Box` -可以重写示例 15-8 中的代码,使用 `Box` 来代替引用,解引用运算符也一样能工作,如示例 15-9 所示: +可以使用 `Box` 代替引用来重写示例 15-6 中的代码,解引用运算符也一样能工作,如示例 15-7 所示: 文件名: src/main.rs @@ -93,15 +62,15 @@ fn main() { } ``` -示例 15-9:在 `Box` 上使用解引用运算符 +示例 15-7:在 `Box` 上使用解引用运算符 -相比示例 15-8 唯一修改的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。让我们通过实现自己的 box 类型来探索 `Box` 能这么做有何特殊之处。 +示例 15-7 相比示例 15-6 唯一不同的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。接下来让我们通过实现自己的 box 类型来探索 `Box` 能这么做有何特殊之处。 ### 自定义智能指针 为了体会默认智能指针的行为不同于引用,让我们创建一个类似于标准库提供的 `Box` 类型的智能指针。接着会学习如何增加使用解引用运算符的功能。 -从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-10 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: +从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: 文件名: src/main.rs @@ -115,15 +84,15 @@ impl MyBox { } ``` -示例 15-10:定义 `MyBox` 类型 +示例 15-8:定义 `MyBox` 类型 -这里定义了一个结构体 `MyBox` 并声明了一个泛型 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。 +这里定义了一个结构体 `MyBox` 并声明了一个泛型参数 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。 -尝试将示例 15-9 中的代码加入示例 15-10 中并修改 `main` 使用我们定义的 `MyBox` 类型代替 `Box`。示例 15-11 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`: +尝试将示例 15-7 中的代码加入示例 15-8 中并修改 `main` 使用我们定义的 `MyBox` 类型代替 `Box`。示例 15-9 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 5; let y = MyBox::new(x); @@ -133,23 +102,23 @@ fn main() { } ``` -示例 15-11:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` +示例 15-9:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` 得到的编译错误是: ```text -error: type `MyBox<{integer}>` cannot be dereferenced +error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 | 14 | assert_eq!(5, *y); | ^^ ``` -`MyBox` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,可以实现 `Deref` trait。 +`MyBox` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,需要实现 `Deref` trait。 -### 实现 `Deref` trait 定义如何像引用一样对待某类型 +### 通过实现 `Deref` trait 将某类型像引用一样处理 -如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-12 包含定义于 `MyBox` 之上的 `Deref` 实现: +如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: 文件名: src/main.rs @@ -166,49 +135,34 @@ impl Deref for MyBox { } ``` -示例 15-12:`MyBox` 上的 `Deref` 实现 +示例 15-10:`MyBox` 上的 `Deref` 实现 `type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。 - - +`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-9 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! -`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-11 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! +没有 `Deref` trait 的话,编译器只会解引用 `&` 引用类型。`deref` 方法向编译器提供了获取任何实现了 `Deref` trait 的类型的值并调用这个类型的 `deref` 方法来获取一个它知道如何解引用的 `&` 引用的能力。 -没有 `Deref` trait 的话,编译器可以解引用的只有 `&` 引用类型;有了 `Deref` trait 之后,对任何实现 `Deref` trait 的类型,编译器都能(通过解引用的形式)从其获取一个值。只要调用这个类型的 `deref` 方法,编译器就可以得到一个 `&` 引用,再对 `&` 引用进行解引用对它来说就是熟悉的操作了。 - -当我们在示例 15-11 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: +当我们在示例 15-9 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: ```rust,ignore *(y.deref()) ``` - - - Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行直接引用的操作,如此我们便不用担心是不是还需要手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。 `deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 -注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-11 中 `assert_eq!` 的 `5` 相匹配。 +注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-9 中 `assert_eq!` 的 `5` 相匹配。 ### 函数和方法的隐式解引用强制多态 - - - **解引用强制多态**(*deref coercions*)是 Rust 表现在函数或方法传参上的一种便利。其将实现了 `Deref` 的类型的引用转换为原始类型通过 `Deref` 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 `deref` 方法被调用,把我们提供的类型转换成了参数所需的类型。 解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&` 和 `*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。 -作为展示解引用强制多态的实例,让我们使用示例 15-10 中定义的 `MyBox`,以及示例 15-12 中增加的 `Deref` 实现。示例 15-13 展示了一个有着字符串 slice 参数的函数定义: +作为展示解引用强制多态的实例,让我们使用示例 15-8 中定义的 `MyBox`,以及示例 15-10 中增加的 `Deref` 实现。示例 15-11 展示了一个有着字符串 slice 参数的函数定义: 文件名: src/main.rs @@ -218,9 +172,9 @@ fn hello(name: &str) { } ``` -示例 15-13:`hello` 函数有着 `&str` 类型的参数 `name` +示例 15-11:`hello` 函数有着 `&str` 类型的参数 `name` -可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox` 类型值的引用调用 `hello` 成为可能,如示例 15-14 所示: +可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox` 类型值的引用调用 `hello` 成为可能,如示例 15-12 所示: 文件名: src/main.rs @@ -253,11 +207,11 @@ fn main() { } ``` -示例 15-14:因为解引用强制多态,使用 `MyBox` 的引用调用 `hello` 是可行的 +示例 15-12:因为解引用强制多态,使用 `MyBox` 的引用调用 `hello` 是可行的 -这里使用 `&m` 调用 `hello` 函数,其为 `MyBox` 值的引用。因为示例 15-12 中在 `MyBox` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。 +这里使用 `&m` 调用 `hello` 函数,其为 `MyBox` 值的引用。因为示例 15-10 中在 `MyBox` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。 -如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox` 类型的值调用 `hello`,则不得不编写示例 15-15 中的代码来代替示例 15-14: +如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox` 类型的值调用 `hello`,则不得不编写示例 15-13 中的代码来代替示例 15-12: 文件名: src/main.rs @@ -290,7 +244,7 @@ fn main() { } ``` -示例 15-15:如果 Rust 没有解引用强制多态则必须编写的代码 +示例 15-13:如果 Rust 没有解引用强制多态则必须编写的代码 `(*m)` 将 `MyBox` 解引用为 `String`。接着 `&` 和 `[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有解引用强制多态所有这些符号混在一起将更难以读写和理解。解引用强制多态使得 Rust 自动的帮我们处理这些转换。 @@ -298,33 +252,14 @@ fn main() { ### 解引用强制多态如何与可变性交互 - - - 类似于如何使用 `Deref` trait 重载不可变引用的 `*` 运算符,Rust 提供了 `DerefMut` trait 用于重载可变引用的 `*` 运算符。 Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态: - - - * 当 `T: Deref` 时从 `&T` 到 `&U`。 * 当 `T: DerefMut` 时从 `&mut T` 到 `&mut U`。 * 当 `T: Deref` 时从 `&mut T` 到 `&U`。 头两个情况除了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以直接得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。 -最后一个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 - - - +第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 diff --git a/src/ch15-03-drop.md b/src/ch15-03-drop.md index ba62d57..633ff3e 100644 --- a/src/ch15-03-drop.md +++ b/src/ch15-03-drop.md @@ -1,18 +1,16 @@ ## 使用 `Drop` Trait 运行清理代码 -> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-03-drop.md) +> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/master/src/ch15-03-drop.md) >
-> commit 6060440d67759b7c8627b4d97cb69576057f5fa6 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -对于智能指针模式来说另一个重要的 trait 是 `Drop`。`Drop` 允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box` 自定义了 `Drop` 用来释放 box 所指向的堆空间。 +对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,`Box` 自定义了 `Drop` 用来释放 box 所指向的堆空间。 -在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码——而且还不会泄露资源了。 - -这意味着无需记住在所有处理完这些类型实例后调用清理代码,而仍然不会泄露资源! +在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定一些代码应该在值离开作用域时被执行,而编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄露资源。 指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。 -示例 15-16 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数: +示例 15-14 展示了唯一定制功能就是当其实例离开作用域时打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`。这会演示 Rust 何时运行 `drop` 函数: 文件名: src/main.rs @@ -52,13 +50,13 @@ Dropping CustomSmartPointer with data `my stuff`! #### 通过 `std::mem::drop` 提早丢弃值 -不幸的是,我们并不能直截了当的禁用 `drop` 这个功能。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop`。 +不幸的是,我们并不能直截了当的禁用 `drop` 这个功能。通常也不需要禁用 `drop` ;整个 `Drop` trait 存在的意义在于其是自动处理的。然而,有时你可能需要提早清理某个值。一个例子是当使用智能指针管理锁时;你可能希望强制运行 `drop` 方法来释放锁以便作用域中的其他代码可以获取锁。Rust 并不允许我们主动调用 `Drop` trait 的 `drop` 方法;当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 `std::mem::drop`。 如果我们像是示例 15-14 那样尝试调用 `Drop` trait 的 `drop` 方法,就会得到像示例 15-15 那样的编译错误: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile fn main() { let c = CustomSmartPointer { data: String::from("some data") }; println!("CustomSmartPointer created."); @@ -83,7 +81,7 @@ error[E0040]: explicit use of destructor method Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结尾对值自动调用 `drop`,这会导致一个 **double free** 错误,因为 Rust 会尝试清理相同的值两次。 -因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要提早清理值,可以使用 `std::mem::drop` 函数。 +因为不能禁用当值离开作用域时自动插入的 `drop`,并且不能显示调用 `drop`,如果我们需要强制提早清理值,可以使用 `std::mem::drop` 函数。 `std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望提早强制丢弃的值作为参数。`std::mem::drop` 位于 prelude,所以我们可以修改示例 15-15 中的 `main` 来调用 `drop` 函数如示例 15-16 所示: @@ -124,6 +122,4 @@ CustomSmartPointer dropped before the end of main. 我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。 -使用 `Drop` trait 实现指定的代码在很多方面都使得清理值变得方便和安全:比如可以使用它来创建我们自己的内存分配器!通过`Drop` trait 和 Rust 所有权系统,就无需担心之后清理代码,因为 Rust 会自动考虑这些问题。如果代码在值仍被使用时就清理它会出现编译错误,因为所有权系统确保了引用总是有效的,这也就保证了`drop`只会在值不再被使用时被调用一次。 - 现在我们学习了 `Box` 和一些智能指针的特性,让我们聊聊一些其他标准库中定义的智能指针。 \ No newline at end of file diff --git a/src/ch15-04-rc.md b/src/ch15-04-rc.md index 5f86fd5..f885a7a 100644 --- a/src/ch15-04-rc.md +++ b/src/ch15-04-rc.md @@ -1,15 +1,11 @@ ## `Rc` 引用计数智能指针 -> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-04-rc.md) +> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/master/src/ch15-04-rc.md) >
-> commit 071b97540bca12fd416d2ea7a2daa5d3e9c74400 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f 大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有。结点直到没有任何边指向它之前都不应该被清理。 - - - 为了启用多所有权,Rust 有一个叫做 `Rc` 的类型。其名称为 **引用计数**(*reference counting*)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。 可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的! @@ -20,19 +16,19 @@ ### 使用 `Rc` 共享数据 -让我们回到示例 15-6 中使用 `Box` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-19 所示: +让我们回到示例 15-5 中使用 `Box` 定义 cons list 的例子。这一次,我们希望创建两个共享第三个列表所有权的列表,其概念将会看起来如图 15-3 所示: Two lists that share ownership of a third list -图 15-19: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权 +图 15-3: 两个列表, `b` 和 `c`, 共享第三个列表 `a` 的所有权 列表 `a` 包含 5 之后是 10,之后是另两个列表:`b` 从 3 开始而 `c` 从 4 开始。`b` 和 `c` 会接上包含 5 和 10 的列表 `a`。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。 -尝试使用 `Box` 定义的 `List` 并实现不能工作,如示例 15-20 所示: +尝试使用 `Box` 定义的 `List` 并实现不能工作,如示例 15-17 所示: 文件名: src/main.rs -```rust,ignore +```rust,ignore,does_not_compile enum List { Cons(i32, Box), Nil, @@ -49,7 +45,7 @@ fn main() { } ``` -示例 15-20: 展示不能用两个 `Box` 的列表尝试共享第三个列表的所有权 +示例 15-17: 展示不能用两个 `Box` 的列表尝试共享第三个列表的所有权 编译会得出如下错误: @@ -62,21 +58,15 @@ error[E0382]: use of moved value: `a` 13 | let c = Cons(4, Box::new(a)); | ^ value used here after move | - = note: move occurs because `a` has type `List`, which does not - implement the `Copy` trait + = note: move occurs because `a` has type `List`, which does not implement + the `Copy` trait ``` `Cons` 成员拥有其储存的数据,所以当创建 `b` 列表时,`a` 被移动进了 `b` 这样 `b` 就拥有了 `a`。接着当再次尝使用 `a` 创建 `c` 时,这不被允许因为 `a` 的所有权已经被移动。 可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。例如,借用检查器不会允许 `let a = Cons(10, &Nil);` 编译,因为临时值 `Nil` 会在 `a` 获取其引用之前就被丢弃了。 -相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如列表 15-21 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理: - - - +相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如列表 15-18 所示。现在每一个 `Cons` 变量都包含一个值和一个指向 `List` 的 `Rc`。当创建 `b` 时,不同于获取 `a` 的所有权,这里会克隆 `a` 所包含的 `Rc`,这会将引用计数从 1 增加到 2 并允许 `a` 和 `b` 共享 `Rc` 中数据的所有权。创建 `c` 时也会克隆 `a`,这会将引用计数从 2 增加为 3。每次调用 `Rc::clone`,`Rc` 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。 文件名: src/main.rs @@ -96,30 +86,17 @@ fn main() { } ``` -示例 15-21: 使用 `Rc` 定义的 `List` +示例 15-18: 使用 `Rc` 定义的 `List` -需要为 `Rc` 增加`use`语句因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。 +需要使用 `use` 语句将 `Rc` 引入作用域因为它不在 prelude 中。在 `main` 中创建了存放 5 和 10 的列表并将其存放在 `a` 的新的 `Rc` 中。接着当创建 `b` 和 `c` 时,调用 `Rc::clone` 函数并传递 `a` 中 `Rc` 的引用作为参数。 -也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间,所以通过使用 `Rc::clone` 进行引用计数,可以明显的区别可能会对运行时性能有巨大影响的深拷贝和不分配内存的对运行时性能影响相对较小的增加引用计数拷贝。 +也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用 `Rc::clone` 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑神拷贝类克隆而无需考虑 `Rc::clone` 调用。 ### 克隆 `Rc` 会增加引用计数 -让我们修改示例 15-21 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。 - - - +让我们修改示例 15-18 的代码以便观察创建和丢弃 `a` 中 `Rc` 的引用时引用计数的变化。 -在示例 15-22 中,修改了 `main` 以便将列表 `c` 置于内部作用域中,这样就可以观察当 `c` 离开作用域时引用计数如何变化。在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。在本章稍后的部分讨论避免引用循环时会解释为何这个函数叫做 `strong_count` 而不是 `count`。 - - - +在示例 15-19 中,修改了 `main` 以便将列表 `c` 置于内部作用域中,这样就可以观察当 `c` 离开作用域时引用计数如何变化。 文件名: src/main.rs @@ -145,9 +122,11 @@ fn main() { } ``` -示例 15-22:打印出引用计数 +示例 15-19:打印出引用计数 + +在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。这个函数叫做 `strong_count` 而不是 `count` 是因为 `Rc` 也有 `weak_count`;在 “避免引用循环” 部分会讲解 `weak_count` 的用途。 -这会打印出: +这段代码会打印出: ```text count after creating a = 1 @@ -156,11 +135,7 @@ count after creating c = 3 count after c goes out of scope = 2 ``` - - - -我们能够看到 `a` 中 `Rc` 的初始引用计数为一,接着每次调用 `clone`,计数会增加一。当 `c` 离开作用域时,计数减一。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc` 值离开作用域时自动减少引用计数。 +我们能够看到 `a` 中 `Rc` 的初始引用计数为一,接着每次调用 `clone`,计数会增加一。当 `c` 离开作用域时,计数减一。不必像调用 `Rc::clone` 增加引用计数那样调用一个函数来减少计数;`Drop` trait 的实现当 `Rc` 值离开作用域时自动减少引用计数。 从这个例子我们所不能看到的是在 `main` 的结尾当 `b` 然后是 `a` 离开作用域时,此处计数会是 0,同时 `Rc` 被完全清理。使用 `Rc` 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。 diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index 05354a9..d169bcc 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -1,24 +1,12 @@ ## `RefCell` 和内部可变性模式 -> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-05-interior-mutability.md) +> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/master/src/ch15-05-interior-mutability.md) >
-> commit 54169ef43f57847913ebec7e021c1267663a5d12 +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f - - + - - - -**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为此,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 +**内部可变性**(*Interior mutability*)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。我们还未讲到不安全代码;第十九章会学习它们。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 `unsafe` 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。 让我们通过遵循内部可变性模式的 `RefCell` 类型来开始探索。 @@ -26,46 +14,32 @@ pattern. /Carol --> 不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第四章所学的借用规则: -1. 在任意给定时间,**只能** 拥有如下中的一个: - * 一个可变引用。 - * 任意数量的不可变引用。 +1. 在任意给定时间,只能拥有一个可变引用或任意数量的不可变引用 **之一**(而不是全部)。 2. 引用必须总是有效的。 -对于引用和 `Box`,借用规则的不可变性作用于编译时。对于 `RefCell`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于`RefCell`,违反这些规则会 `panic!`。 - - - +对于引用和 `Box`,借用规则的不可变性作用于编译时。对于 `RefCell`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于 `RefCell`,如果违反这些规则程序会 panic 并退出。 -在编译时检查借用规则的好处是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。 +在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获同时对没有运行时性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。 -相反在运行时检查借用规则的好处是特定内存安全的场景是允许的,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是 [停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。 +相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。代码的一些属性则不可能通过分析代码发现:其中最著名的就是 [停机问题(Halting Problem)](https://zh.wikipedia.org/wiki/%E5%81%9C%E6%9C%BA%E9%97%AE%E9%A2%98),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。 - - - -因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么人们也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,会给程序员带来不便,但不会带来灾难。`RefCell` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 +因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。`RefCell` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell`,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用 `RefCell` 的功能。 - - - 如下为选择 `Box`,`Rc` 或 `RefCell` 的理由: -- `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 有单一所有者。 -- `Box` 允许在编译时执行不可变(或可变)借用检查;`Rc`仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变(或可变)借用检查。 -- 因为 `RefCell` 允许在运行时执行可变借用检查,所以我们可以在即便 `RefCell` 自身是不可变的情况下修改其内部的值。 +* `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 有单一所有者。 +* `Box` 允许在编译时执行不可变或可变借用检查;`Rc`仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变或可变借用检查。 +* 因为 `RefCell` 允许在运行时执行可变借用检查,所以我们可以在即便 `RefCell` 自身是不可变的情况下修改其内部的值。 -最后一个理由便是指 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 +在不可变值内部改变值就是 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 ### 内部可变性:不可变值的可变借用 借用规则的一个推论是当有一个不可变值时,不能可变的借用它。例如,如下代码不能编译: -```rust,ignore +```rust,ignore,does_not_compile fn main() { let x = 5; let y = &mut x; @@ -96,7 +70,7 @@ error[E0596]: cannot borrow immutable local variable `x` as mutable 如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。 -该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 `Messenger` trait 即可。示例 15-23 展示了库代码: +该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 `Messenger` trait 即可。示例 15-20 展示了库代码: 文件名: src/lib.rs @@ -141,11 +115,11 @@ impl<'a, T> LimitTracker<'a, T> 这些代码中一个重要部分是拥有一个方法 `send` 的 `Messenger` trait,其获取一个 `self` 的不可变引用和文本信息。这是我们的 mock 对象所需要拥有的接口。另一个重要的部分是我们需要测试 `LimitTracker` 的 `set_value` 方法的行为。可以改变传递的 `value` 参数的值,不过 `set_value` 并没有返回任何可供断言的值。也就是说,如果使用某个实现了 `Messenger` trait 的值和特定的 `max` 创建 `LimitTracker`,当传递不同 `value` 值时,消息发送者应被告知发送合适的消息。 -我们所需的 mock 对象是,调用 `send` 不同于实际发送 email 或短息,其只记录信息被通知要发送了。可以新建一个 mock 对象示例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-24 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许: +我们所需的 mock 对象是,调用 `send` 不同于实际发送 email 或短息,其只记录信息被通知要发送了。可以新建一个 mock 对象示例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-21 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许: 文件名: src/lib.rs -```rust +```rust,does_not_compile #[cfg(test)] mod tests { use super::*; @@ -178,7 +152,7 @@ mod tests { } ``` -示例 15-24:尝试实现 `MockMessenger`,借用检查器并不允许 +示例 15-21:尝试实现 `MockMessenger`,借用检查器不允许这么做 测试代码定义了一个 `MockMessenger` 结构体,其 `sent_messages` 字段为一个 `String` 值的 `Vec` 用来记录被告知发送的消息。我们还定义了一个关联函数 `new` 以便于新建从空消息列表开始的 `MockMessenger` 值。接着为 `MockMessenger` 实现 `Messenger` trait 这样就可以为 `LimitTracker` 提供一个 `MockMessenger`。在 `send` 方法的定义中,获取传入的消息作为参数并储存在 `MockMessenger` 的 `sent_messages` 列表中。 @@ -188,17 +162,17 @@ mod tests { ```text error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable - --> src/lib.rs:46:13 + --> src/lib.rs:52:13 | -45 | fn send(&self, message: &str) { +51 | fn send(&self, message: &str) { | ----- use `&mut self` here to make mutable -46 | self.sent_messages.push(String::from(message)); +52 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field ``` 不能修改 `MockMessenger` 来记录消息,因为 `send` 方法获取 `self` 的不可变引用。我们也不能参考错误文本的建议使用 `&mut self` 替代,因为这样 `send` 的签名就不符合 `Messenger` trait 定义中的签名了(请随意尝试如此修改并看看会出现什么错误信息)。 -这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然而 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-25 展示了代码: +这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然而 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-22 展示了代码: 文件名: src/lib.rs @@ -236,7 +210,7 @@ mod tests { } ``` -示例 15-25:使用 `RefCell` 能够在外部值被认为是不可变的情况下修改内部值 +示例 15-22:使用 `RefCell` 能够在外部值被认为是不可变的情况下修改内部值 现在 `sent_messages` 字段的类型是 `RefCell>` 而不是 `Vec`。在 `new` 函数中新建了一个 `RefCell` 示例替代空 vector。 @@ -246,21 +220,17 @@ mod tests { 现在我们见识了如何使用 `RefCell`,让我们研究一下它怎样工作的! -### `RefCell` 在运行时检查借用规则 +### `RefCell` 在运行时记录借用 当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。对于 `RefCell` 来说,则是 `borrow` 和 `borrow_mut` 方法,这属于 `RefCell` 安全 API 的一部分。`borrow` 方法返回 `Ref` 类型的智能指针,`borrow_mut` 方法返回 `RefMut` 类型的智能指针。这两个类型都实现了 `Deref` 所以可以当作常规引用对待。 - - - -`RefCell` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell` 在任何时候只允许有多个不可变借用或一个可变借用。 +`RefCell` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell` 在任何时候只允许有多个不可变借用或一个可变借用。 -如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时 `panic!`。示例 15-26 展示了对示例 15-25 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做: +如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时 `panic!`。示例 15-23 展示了对示例 15-22 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做: 文件名: src/lib.rs -```rust,ignore +```rust,ignore,panics impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); @@ -274,27 +244,24 @@ impl Messenger for MockMessenger { 示例 15-26:在同一作用域中创建两个可变引用并观察 `RefCell` panic -这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的,如果运行库的测试,编译时不会有任何错误,不过测试会失败: +这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建一个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败: ```text ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at - 'already borrowed: BorrowMutError', src/libcore/result.rs:906:4 +'already borrowed: BorrowMutError', src/libcore/result.rs:906:4 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -可以看到代码 panic 和信息`already borrowed: BorrowMutError`。这也就是 `RefCell` 如何在运行时处理违反借用规则的情况。 +注意代码 panic 和信息 `already borrowed: BorrowMutError`。这也就是 `RefCell` 如何在运行时处理违反借用规则的情况。 -在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误 ———— 甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell` 来获得比常规引用所能提供的更多的功能。 +在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 `RefCell` 使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用 `RefCell` 来获得比常规引用所能提供的更多的功能。 ### 结合 `Rc` 和 `RefCell` 来拥有多个可变数据所有者 `RefCell` 的一个常见用法是与 `Rc` 结合。回忆一下 `Rc` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell` 的 `Rc` 的话,就可以得到有多个所有者 **并且** 可以修改的值了! - - - -例如,回忆示例 15-13 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-27 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: +例如,回忆示例 15-18 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-24 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: 文件名: src/main.rs @@ -325,17 +292,13 @@ fn main() { } ``` -示例 15-27:使用 `Rc>` 创建可以修改的 `List` +示例 15-24:使用 `Rc>` 创建可以修改的 `List` 这里创建了一个 `Rc` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。 - - - -我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-13 一样。 +我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,他们都可以引用 `a`,正如示例 15-18 一样。 -一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自定解引用功能(“`->`运算符到哪去了?”)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 +一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能(“`->` 运算符到哪去了?” 部分)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 当我们打印出 `a`、`b` 和 `c` 时,可以看到他们都拥有修改后的值 15 而不是 5: diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index 49fc038..f948b4d 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -1,18 +1,22 @@ ## 引用循环与内存泄漏 -> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-06-reference-cycles.md) +> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/master/src/ch15-06-reference-cycles.md) >
-> commit cd7d9bcfb099c224439db0ba3b02956d9843864d +> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f -Rust 的内存安全保证使其 **难以** 意外的制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 +Rust 的内存安全保证使其难以意外的制造永远也不会被清理的内存(被称为 **内存泄露**(*memory leak*)),但并不是不可能。完全的避免内存泄露并不是同在编译时拒绝数据竞争一样为 Rust 的保证之一,这意味着内存泄露在 Rust 被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:有可能会创建个个项之间相互引用的引用。这会造成内存泄露,因为每一项的引用计数将永远也到不了 0,其值也永远也不会被丢弃。 ### 制造引用循环 -让我们看看引用循环是如何发生的以及如何避免它。以示例 15-28 中的 `List` 枚举和 `tail` 方法的定义开始: +让我们看看引用循环是如何发生的以及如何避免它。以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始: 文件名: src/main.rs -```rust,ignore + + +```rust +# fn main() {} use std::rc::Rc; use std::cell::RefCell; use List::{Cons, Nil}; @@ -25,33 +29,19 @@ enum List { impl List { fn tail(&self) -> Option<&RefCell>> { - match *self { - Cons(_, ref item) => Some(item), + match self { + Cons(_, item) => Some(item), Nil => None, } } } ``` -示例:一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据 - -这里采用了示例 15-6 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell>`,这意味着不同于像示例 15-19 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。 +示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据 - - +这里采用了示例 15-25 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。 -在示例 15-29 中增加了一个 `main` 函数,其使用了示例 15-28 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `b` 中的列表指向 `a` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 - - - +在示例 15-26 中增加了一个 `main` 函数,其使用了示例 15-25 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `b` 中的列表指向 `a` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 Filename: src/main.rs @@ -67,8 +57,8 @@ file --> # # impl List { # fn tail(&self) -> Option<&RefCell>> { -# match *self { -# Cons(_, ref item) => Some(item), +# match self { +# Cons(_, item) => Some(item), # Nil => None, # } # } @@ -93,18 +83,17 @@ fn main() { println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); - // Uncomment the next line to see that we have a cycle; it will - // overflow the stack + // 取消如下行的注释来观察引用循环; + // 这会导致栈溢出 // println!("a next item = {:?}", a.tail()); } ``` -示例 15-29:创建一个引用循环:两个`List` 值互相指向彼此 - -这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a` 的 `List` 的另一个 `Rc` 实例。 +示例 15-26:创建一个引用循环:两个 `List` 值互相指向彼此 -最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell` 的引用,并放入变量 `link` 中。接着使用 `RefCell` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 +这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a` 的 `List` 的另一个 `Rc` 实例。 +最后,修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell>` 的引用,并放入变量 `link` 中。接着使用 `RefCell>` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 如果保持最后的 `println!` 行注释并运行代码,会得到如下输出: @@ -118,74 +107,29 @@ b rc count after changing a = 2 a rc count after changing a = 2 ``` -可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc` 实例的引用计数为 2。在 `main` 的结尾,Rust 会尝试首先丢弃 `b`,这会使 `a` 和 `b` 中 `Rc` 实例的引用计数减一。 +可以看到将 `a` 修改为指向 `b` 之后,`a` 和 `b` 中都有的 `Rc` 实例的引用计数为 2。在 `main` 的结尾,Rust 会尝试首先丢弃 `b`,这会使 `a` 和 `b` 中 `Rc` 实例的引用计数减 1。 - - - - - - -然而,因为 `a` 仍然引用 `b` 中的 `Rc`,`Rc` 的引用计数是 1 而不是 0,所以 `Rc` 在堆上的内存不会被丢弃。其内存会因为引用计数为 1 而永远停留。 - -为了更形象的展示,我们创建了一个如图 15-30 所示的引用循环: +然而,因为 `a` 仍然引用 `b` 中的 `Rc`,`Rc` 的引用计数是 1 而不是 0,所以 `Rc` 在堆上的内存不会被丢弃。其内存会因为引用计数为 1 而永远停留。为了更形象的展示,我们创建了一个如图 15-4 所示的引用循环: Reference cycle of lists -图 15-30: 列表 `a` 和 `b` 彼此互相指向形成引用循环 +图 15-4: 列表 `a` 和 `b` 彼此互相指向形成引用循环 如果取消最后 `println!` 的注释并运行程序,Rust 会尝试打印出 `a` 指向 `b` 指向 `a` 这样的循环直到栈溢出。 - - - 这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。 -创建引用循环并不容易,但也不是不可能。如果你有包含`Rc`的`RefCell`值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 +创建引用循环并不容易,但也不是不可能。如果你有包含 `Rc` 的 `RefCell` 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 - - - -另一个解决方案是重新组织数据结构使得一些引用有所有权而另一些则没有。如此,循环将由一些有所有权的关系和一些没有所有权的关系,而只有所有权关系才影响值是否被丢弃。在示例 15-28 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父结点和子结点构成的图的例子,观察何时无所有权关系是一个好的避免引用循环的方法。 +另一个解决方案是重新组织数据结构使得一些引用有所有权而另一些则没有。如此,循环将由一些有所有权的关系和一些没有所有权的关系,而只有所有权关系才影响值是否被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父结点和子结点构成的图的例子,观察何时无所有权关系是一个好的避免引用循环的方法。 ### 避免引用循环:将 `Rc` 变为 `Weak` -到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和 `Rc` 实例只在其 `strong_count` 为 0 时才会被清理。也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**(*weak reference*)。调用 `Rc::downgrade` 时会得到 `Weak` 类型的智能指针。不同于将 `Rc` 实例的 `strong_count` 加一,调用 `Rc::downgrade` 会将 `weak_count` 加一。`Rc` 类型使用 `weak_count` 来记录其存在多少个 `Weak` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc` 实例被清理。 - - - +到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和 `Rc` 实例只在其 `strong_count` 为 0 时才会被清理。也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**(*weak reference*)。调用 `Rc::downgrade` 时会得到 `Weak` 类型的智能指针。不同于将 `Rc` 实例的 `strong_count` 加一,调用 `Rc::downgrade` 会将 `weak_count` 加一。`Rc` 类型使用 `weak_count` 来记录其存在多少个 `Weak` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc` 实例被清理。 -强引用代表如何共享 `Rc` 实例的引用。弱引用并不代表所有权关系。他们不会造成引用循环,因为任何引入了弱引用的循环一旦所涉及的强引用计数为 0 就会被打破。 +强引用代表如何共享 `Rc` 实例的所有权。弱引用并不代表所有权关系。他们不会造成引用循环,因为任何引入了弱引用的循环一旦所涉及的强引用计数为 0 就会被打破。 - - - -因为 `Weak` 引用的值可能已经被丢弃了,为了使用 `Weak` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak` 实例的 `upgrade` 方法,这会返回 `Option>`。如果 `Rc` 值还未被丢弃则结果是 `Some`,如果 `Rc` 已经被丢弃则结果是 `None`。因为 `upgrade` 返回一个 `Option`,我们确信 Rust 会处理 `Some` 和 `None`的情况,并且不会有一个无效的指针。 +因为 `Weak` 引用的值可能已经被丢弃了,为了使用 `Weak` 所指向的值,我们必须确保其值仍然有效。为此可以调用 `Weak` 实例的 `upgrade` 方法,这会返回 `Option>`。如果 `Rc` 值还未被丢弃则结果是 `Some`,如果 `Rc` 已经被丢弃则结果是 `None`。因为 `upgrade` 返回一个 `Option`,我们确信 Rust 会处理 `Some` 和 `None`的情况,并且不会有一个无效的指针。 作为一个例子,不同于使用一个某项只知道其下一项的列表,我们会创建一个某项知道其子项 **和** 父项的树形结构。 @@ -206,9 +150,9 @@ struct Node { } ``` -我们希望能够 `Node` 拥有其子结点,同时也希望变量可以拥有每个结点以便可以直接访问他们。为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能改其他结点的子结点,所以 `children` 中 `Vec` 被放进了 `RefCell`。 +我们希望能够 `Node` 拥有其子结点,同时也希望通过变量来共享所有权,以便可以直接访问树种的每一个 `Node`。为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能改其他结点的子结点,所以 `children` 中 `Vec>` 被放进了 `RefCell`。 -接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子结点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子结点的实例 `branch`,如示例 15-31 所示: +接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子结点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子结点的实例 `branch`,如示例 15-27 所示: 文件名: src/main.rs @@ -235,9 +179,9 @@ fn main() { } ``` -示例 15-31:创建没有子结点的 `leaf` 结点和以 `leaf` 作为子结点的 `branch` 结点 +示例 15-27:创建没有子结点的 `leaf` 结点和以 `leaf` 作为子结点的 `branch` 结点 -这里克隆了 `leaf` 中的 `Rc` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父结点。 +这里克隆了 `leaf` 中的 `Rc` 并储存在了 `branch` 中,这意味着 `leaf` 中的 `Node` 现在有两个所有者:`leaf`和`branch`。可以通过 `branch.children` 从 `branch` 中获得 `leaf`,不过无法从 `leaf` 到 `branch`。`leaf` 没有到 `branch` 的引用且并不知道他们相互关联。我们希望 `leaf` 知道 `branch` 是其父结点。稍后会这么做。 #### 增加从子到父的引用 @@ -245,12 +189,7 @@ fn main() { 现在换一种方式思考这个关系,父结点应该拥有其子结点:如果父结点被丢弃了,其子结点也应该被丢弃。然而子结点不应该拥有其父结点:如果丢弃子结点,其父结点应该依然存在。这正是弱引用的例子! -所以 `parent` 使用 `Weak` 类型而不是 `Rc`,具体来说是 `RefCell>`。现在 `Node` 结构体定义看起来像这样: - - - +所以 `parent` 使用 `Weak` 类型而不是 `Rc`,具体来说是 `RefCell>`。现在 `Node` 结构体定义看起来像这样: 文件名: src/main.rs @@ -266,19 +205,7 @@ struct Node { } ``` - - - -这样,一个结点就能够引用其父结点,但不拥有其父结点。在示例 15-32 中,我们更新 `main` 来使用新定义以便 `leaf` 结点可以引用其父结点: - - - +这样,一个结点就能够引用其父结点,但不拥有其父结点。在示例 15-28 中,我们更新 `main` 来使用新定义以便 `leaf` 结点可以通过 `branch` 引用其父结点: 文件名: src/main.rs @@ -314,13 +241,9 @@ fn main() { } ``` -示例 15-32:一个 `leaf` 结点,其拥有指向其父结点 `branch` 的 `Weak` 引用 - - - +示例 15-28:一个 `leaf` 结点,其拥有指向其父结点 `branch` 的 `Weak` 引用 -创建 `leaf` 结点类似于示例 15-31 中如何创建 `leaf` 结点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父结点,所以我们新建了一个空的 `Weak` 引用实例。 +创建 `leaf` 结点类似于示例 15-27 中如何创建 `leaf` 结点的,除了 `parent` 字段有所不同:`leaf` 开始时没有父结点,所以我们新建了一个空的 `Weak` 引用实例。 此时,当尝试使用 `upgrade` 方法获取 `leaf` 的父结点引用时,会得到一个 `None` 值。如第一个 `println!` 输出所示: @@ -328,18 +251,9 @@ talk it through --> leaf parent = None ``` - - +当创建 `branch` 结点时,其也会新建一个 `Weak` 引用,因为 `branch` 并没有父结点。`leaf` 仍然作为 `branch` 的一个子结点。一旦在 `branch` 中有了 `Node` 实例,就可以修改 `leaf` 使其拥有指向父结点的 `Weak` 引用。这里使用了 `leaf` 中 `parent` 字段里的 `RefCell>` 的 `borrow_mut` 方法,接着使用了 `Rc::downgrade` 函数来从 `branch` 中的 `Rc` 值创建了一个指向 `branch` 的 `Weak` 引用。 -当创建 `branch` 结点时,其也会新建一个 `Weak` 引用,因为 `branch` 并没有父结点。`leaf` 仍然作为 `branch` 的一个子结点。一旦在 `branch` 中有了 `Node` 实例,就可以修改 `leaf` 使其拥有指向父结点的 `Weak` 引用。这里使用了 `leaf` 中 `parent` 字段里的 `RefCell` 的 `borrow_mut` 方法,接着使用了 `Rc::downgrade` 函数来从 `branch` 中的 `Rc` 值创建了一个指向 `branch` 的 `Weak` 引用。 - - - - -当再次打印出 `leaf` 的父结点时,这一次将会得到存放了 `branch` 的 `Some` 值:现在 `leaf` 可以访问其父结点了!当打印出 `leaf` 时,我们也避免了如示例 15-29 中最终会导致栈溢出的循环:`Weak` 引用被打印为 `(Weak)`: +当再次打印出 `leaf` 的父结点时,这一次将会得到存放了 `branch` 的 `Some` 值:现在 `leaf` 可以访问其父结点了!当打印出 `leaf` 时,我们也避免了如示例 15-26 中最终会导致栈溢出的循环:`Weak` 引用被打印为 `(Weak)`: ```text leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, @@ -351,11 +265,21 @@ children: RefCell { value: [] } }] } }) #### 可视化 `strong_count` 和 `weak_count` 的改变 -让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc` 实例的 `strong_count` 和 `weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-33 所示: +让我们通过创建了一个新的内部作用域并将 `branch` 的创建放入其中,来观察 `Rc` 实例的 `strong_count` 和 `weak_count` 值的变化。这会展示当 `branch` 创建和离开作用域被丢弃时会发生什么。这些修改如示例 15-29 所示: 文件名: src/main.rs -```rust,ignore +```rust +# use std::rc::{Rc, Weak}; +# use std::cell::RefCell; +# +# #[derive(Debug)] +# struct Node { +# value: i32, +# parent: RefCell>, +# children: RefCell>>, +# } +# fn main() { let leaf = Rc::new(Node { value: 3, @@ -375,6 +299,7 @@ fn main() { parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); + *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( @@ -399,25 +324,15 @@ fn main() { } ``` -示例 15-33:在内部作用域创建 `branch` 并检查其强弱引用计数 - -一旦创建了 `leaf`,其 `Rc` 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 `branch` 并与 `leaf` 相关联,此时 `branch` 中 `Rc` 的强引用计数为 1,弱引用计数为 1(因为 `leaf.parent` 通过 `Weak` 指向 `branch`)。这里 `leaf` 的强引用计数为 2,因为现在 `branch` 的 `branch.children` 中储存了 `leaf` 的 `Rc` 的拷贝,不过弱引用计数仍然为 0。 - -当内部作用域结束时,`branch` 离开作用域,其强引用计数减少为 0,所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄露! +示例 15-29:在内部作用域创建 `branch` 并检查其强弱引用计数 -如果在内部作用域结束后尝试访问 `leaf` 的父结点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc` 的强引用计数为 1,弱引用计数为 0,因为现在 `leaf` 又是 `Rc` 唯一的引用了。 +一旦创建了 `leaf`,其 `Rc` 的强引用计数为 1,弱引用计数为 0。在内部作用域中创建了 `branch` 并与 `leaf` 相关联,此时 `branch` 中 `Rc` 的强引用计数为 1,弱引用计数为 1(因为 `leaf.parent` 通过 `Weak` 指向 `branch`)。这里 `leaf` 的强引用计数为 2,因为现在 `branch` 的 `branch.children` 中储存了 `leaf` 的 `Rc` 的拷贝,不过弱引用计数仍然为 0。 - - +当内部作用域结束时,`branch` 离开作用域,`Rc` 的强引用计数减少为 0,所以其 `Node` 被丢弃。来自 `leaf.parent` 的弱引用计数 1 与 `Node` 是否被丢弃无关,所以并没有产生任何内存泄露! -所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子结点到父结点的关系为一个`Weak`引用,就能够拥有父结点和子结点之间的双向引用而不会造成引用循环和内存泄露。 +如果在内部作用域结束后尝试访问 `leaf` 的父结点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc` 的强引用计数为 1,弱引用计数为 0,因为现在 `leaf` 又是 `Rc` 唯一的引用了。 - - +所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子结点到父结点的关系为一个`Weak`引用,就能够拥有父结点和子结点之间的双向引用而不会造成引用循环和内存泄露。 ## 总结 @@ -429,4 +344,4 @@ like that to the start of the Weak section? --> [“The Nomicon”]: https://doc.rust-lang.org/stable/nomicon/ -接下来,让我们谈谈 Rust 的并发。我们还会学习到一些新的对并发有帮助的智能指针。 +接下来,让我们谈谈 Rust 的并发。届时甚至还会学习到一些新的对并发有帮助的智能指针。