diff --git a/src/ch14-04-installing-binaries.md b/src/ch14-04-installing-binaries.md index 05d05f0..9e0ce82 100644 --- a/src/ch14-04-installing-binaries.md +++ b/src/ch14-04-installing-binaries.md @@ -1,26 +1,26 @@ ## 使用 `cargo install` 安装二进制文件 -> [ch14-04-installing-binaries.md](https://github.com/rust-lang/book/blob/main/src/ch14-04-installing-binaries.md) ->
-> commit 704c51eec2f26a0133ae17a2c01986590c05a045 + + `cargo install` 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 [crates.io](https://crates.io/) 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。**二进制目标** 文件是在 crate 有 *src/main.rs* 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标文件。通常 crate 的 *README* 文件中有该 crate 是库、二进制目标还是两者兼有的信息。 -所有来自 `cargo install` 的二进制文件都安装到 Rust 安装根目录的 *bin* 文件夹中。如果你是使用 *rustup.rs* 来安装 Rust 且没有自定义任何配置,这个目录将是 `$HOME/.cargo/bin`。确保将这个目录添加到 `$PATH` 环境变量中就能够运行通过 `cargo install` 安装的程序了。 +所有来自 `cargo install` 的二进制文件都安装到 Rust 安装根目录的 *bin* 文件夹中。如果你是使用 *rustup.rs* 来安装 Rust 且没有自定义任何配置,这个目录将是 *$HOME/.cargo/bin*。确保将这个目录添加到 `$PATH` 环境变量中就能够运行通过 `cargo install` 安装的程序了。 例如,第十二章提到的叫做 `ripgrep` 的用于搜索文件的 `grep` 的 Rust 实现。为了安装 `ripgrep` 运行如下: ```console $ cargo install ripgrep Updating crates.io index - Downloaded ripgrep v13.0.0 - Downloaded 1 crate (243.3 KB) in 0.88s - Installing ripgrep v13.0.0 + Downloaded ripgrep v14.1.1 + Downloaded 1 crate (213.6 KB) in 0.40s + Installing ripgrep v14.1.1 --snip-- - Compiling ripgrep v13.0.0 - Finished release [optimized + debuginfo] target(s) in 3m 10s + Compiling grep v0.3.2 + Finished `release` profile [optimized + debuginfo] target(s) in 6.73s Installing ~/.cargo/bin/rg - Installed package `ripgrep v13.0.0` (executable `rg`) + Installed package `ripgrep v14.1.1` (executable `rg`) ``` -最后一行输出展示了安装的二进制文件的位置和名称,在这里 `ripgrep` 被命名为 `rg`。只要你像上面提到的那样将安装目录加入 `$PATH`,就可以运行 `rg --help` 并开始使用一个更快更 Rust 的工具来搜索文件了! +倒数第二行输出展示了安装的二进制文件的位置和名称,在这里 `ripgrep` 被命名为 `rg`。只要你像上面提到的那样将安装目录加入 `$PATH`,就可以运行 `rg --help` 并开始使用一个更快更 Rust 的工具来搜索文件了! + diff --git a/src/ch14-05-extending-cargo.md b/src/ch14-05-extending-cargo.md index 1f03d51..950789d 100644 --- a/src/ch14-05-extending-cargo.md +++ b/src/ch14-05-extending-cargo.md @@ -1,11 +1,10 @@ ## Cargo 自定义扩展命令 -> [ch14-05-extending-cargo.md](https://github.com/rust-lang/book/blob/main/src/ch14-05-extending-cargo.md) ->
-> commit c084bdd9ee328e7e774df19882ccc139532e53d8 + + -Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改 Cargo 本身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行它们是 Cargo 设计上的一个非常方便的优点! +Cargo 的设计使得开发者可以通过新的子命令来对 Cargo 进行扩展,而无需修改其本身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行它们是 Cargo 设计上的一个非常方便的优点! ## 总结 -通过 Cargo 和 [crates.io](https://crates.io/) 来分享代码是使得 Rust 生态环境可以用于许多不同的任务的重要组成部分。Rust 的标准库是小而稳定的,不过 crate 易于分享和使用,并采用一个不同于语言自身的时间线来提供改进。不要羞于在 [crates.io](https://crates.io/) 上共享对你有用的代码,因为它很有可能对别人也很有用! +通过 Cargo 和 [crates.io](https://crates.io/) 来分享代码是使得 Rust 生态环境可以用于许多不同的任务的重要组成部分。Rust 的标准库是小而稳定的,不过 crate 易于分享和使用,并采用一个不同于语言自身的时间线来提供改进。不要犹豫在 [crates.io](https://crates.io/) 上共享对你有用的代码,因为它很有可能对别人也很有用! diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index 8fe140d..f8edfc2 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -1,23 +1,24 @@ # 智能指针 -> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/main/src/ch15-00-smart-pointers.md) ->
-> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e + + -**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。 +**指针**(*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其它数据。Rust 中最常见的指针是第四章介绍的**引用**(*reference*)。引用以 `&` 符号为标志并借用了它们所指向的值。除了引用数据没有任何其他特殊功能,也没有额外开销。 -另一方面,**智能指针**(*smart pointers*)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。为了探索其基本概念,我们来看看一些智能指针的例子,这包括 **引用计数** (*reference counting*)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。在 Rust 中因为引用和借用,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针 **拥有** 它们指向的数据。 +另一方面,**智能指针**(*smart pointers*)是一类数据结构,它们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其它语言中。Rust 标准库中定义了多种不同的智能指针,它们提供了多于引用的额外功能。为了探索其基本概念,我们来看看一些智能指针的例子,这包括**引用计数** (*reference counting*)智能指针类型。这种指针允许数据有多个所有者,它会记录所有者的数量,当没有所有者时清理数据。 + +在 Rust 中因为引用和借用的概念,普通引用和智能指针有一个额外的区别:引用是一类只借用数据的指针,在大部分情况下,智能指针**拥有**它们指向的数据。 实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec`,虽然当时并没有这样称呼它们。这些类型都属于智能指针,因为它们拥有一些数据,并允许你修改这些数据。它们也拥有元数据和额外的功能或保证。例如 `String` 存储了其容量作为元数据,并拥有额外的能力来确保其数据总是有效的 UTF-8 编码。 智能指针通常使用结构体实现。智能指针不同于结构体的地方在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说它们很重要。 -考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些: +考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的几种: -* `Box`,用于在堆上分配值 -* `Rc`,一个引用计数类型,其数据可以有多个所有者 -* `Ref` 和 `RefMut`,通过 `RefCell` 访问。( `RefCell` 是一个在运行时而不是在编译时执行借用规则的类型)。 +- `Box`,用于在堆上分配值 +- `Rc`,一个引用计数类型,其数据可以有多个所有者 +- `Ref` 和 `RefMut`,通过 `RefCell` 访问。`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 92cf6c7..18b12c8 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -1,22 +1,21 @@ -## 使用`Box`指向堆上的数据 +## 使用 `Box` 指向堆上的数据 -> [ch15-01-box.md](https://github.com/rust-lang/book/blob/main/src/ch15-01-box.md) ->
-> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e + + 最简单直接的智能指针是 _box_,其类型是 `Box`。box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 除了数据被储存在堆上而不是栈上之外,box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景: -* 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 -* 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 -* 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 +- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 +- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 +- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 -我们会在 [“box 允许创建递归类型”](#box-允许创建递归类型) 部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(_trait object_),第十八章刚好有一整个部分 [“顾及不同类型值的 trait 对象”][trait-objects] 专门讲解这个主题。所以这里所学的内容会在第十八章再次用上! +我们会在[“box 允许创建递归类型”](#box-允许创建递归类型)部分展示第一种场景。在第二种情况中,转移大量数据的所有权可能会花费很长的时间,因为数据在栈上进行了拷贝。为了改善这种情况下的性能,可以通过 box 将这些数据储存在堆上。接着,只有少量的指针数据在栈上被拷贝。第三种情况被称为 **trait 对象**(*trait object*),第十八章刚好有一整个部分[“顾及不同类型值的 trait 对象”][trait-objects]专门讲解这个主题。所以这里所学的内容会在第十八章再次应用! -### 使用 `Box` 在堆上储存数据 +### 使用 `Box` 在堆上存储数据 -在讨论 `Box` 的堆存储用例之前,让我们熟悉一下语法以及如何与储存在 `Box` 中的值进行交互。 +在讨论 `Box` 的堆存储用例之前,让我们熟悉一下语法以及如何与存储在 `Box` 中的值进行交互。 示例 15-1 展示了如何使用 box 在堆上储存一个 `i32`: @@ -36,21 +35,21 @@ **递归类型**(_recursive type_)的值可以拥有另一个同类型的值作为其自身的一部分。但是这会产生一个问题,因为 Rust 需要在编译时知道类型占用多少空间。递归类型的值嵌套理论上可以无限地进行下去,所以 Rust 不知道递归类型需要多少空间。因为 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。 -作为一个递归类型的例子,让我们探索一下 _cons list_。这是一个函数式编程语言中常见的数据类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念,在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 +作为一个递归类型的例子,让我们探索一下 _cons list_。这是一个函数式编程语言中常见的数据类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型相当简单,所以这个例子中的概念,在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 -#### cons list 的更多内容 +#### cons list 的更多信息 _cons list_ 是一个来源于 Lisp 编程语言及其方言的数据结构,它由嵌套的列表组成。它的名字来源于 Lisp 中的 `cons` 函数(“construct function" 的缩写),它利用两个参数来构造一个新的列表。通过对一个包含值的列表和另一个值调用 `cons`,可以构建由递归列表组成的 cons list。 -例如这里有一个包含列表 1,2,3 的 cons list 的伪代码表示,其每一个列表在一个括号中: +例如这里有一个包含列表 `1, 2, 3` 的 cons list 的伪代码表示,其每个对在一个括号中: ```text (1, (2, (3, Nil))) ``` -cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,它们代表无效或缺失的值。 +cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值且没有下一项。cons list 通过递归调用 `cons` 函数产生。代表递归的归基条件(base case)的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,它们代表无效或缺失的值。 -cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 +cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型**确实**在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力地定义一个递归数据类型。 示例 15-2 包含一个 cons list 的枚举定义。注意这还不能编译因为这个类型没有已知的大小,之后我们会展示: @@ -74,7 +73,7 @@ cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要 示例 15-3:使用 `List` 枚举储存列表 `1, 2, 3` -第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2` 的 `Cons` 值和下一个 `List` 值。接着又有另一个存放了 `3` 的 `Cons` 值和最后一个值为 `Nil` 的 `List`,非递归成员代表了列表的结尾。 +第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2` 的 `Cons` 值和下一个 `List` 值。接着又有另一个存放了 `3` 的 `Cons` 值和最后一个值为 `Nil` 的 `List`,非递归变体代表了列表的结尾。 如果尝试编译示例 15-3 的代码,会得到如示例 15-4 所示的错误: @@ -84,7 +83,7 @@ cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要 示例 15-4:尝试定义一个递归枚举时得到的错误 -这个错误表明这个类型 “有无限的大小”。其原因是 `List` 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们拆开来看为何会得到这个错误。首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 +这个错误表明这个类型 “有无限的大小”(“has infinite size”)。其原因是 `List` 的一个变体被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们拆开来看为何会得到这个错误。首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 ### 计算非递归类型的大小 @@ -94,28 +93,28 @@ cons list 并不是一个 Rust 中常见的类型。大部分在 Rust 中需要 {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}} ``` -当 Rust 需要知道要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因为 enum 实际上只会使用其中的一个成员,所以 `Message` 值所需的空间等于储存其最大成员的空间大小。 +当 Rust 需要知道要为 `Message` 值分配多少空间时,它可以检查每一个变体并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因为 enum 实际上只会使用其中的一个变体,所以 `Message` 值所需的空间等于储存其最大变体的空间大小。 -与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-1 所示: +与此相对当 Rust 编译器检查像示例 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器从 `Cons` 变体开始分析,其需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,编译器检查其变体,从 `Cons` 变体开始。`Cons` 变体储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-1 所示: An infinite Cons list -图 15-1:一个包含无限个 `Cons` 成员的无限 `List` +图 15-1:一个包含无限个 `Cons` 变体的无限 `List` ### 使用 `Box` 给递归类型一个已知的大小 因为 Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了一个包括了有用建议的错误: ```text -help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable +help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle | 2 | Cons(i32, Box), | ++++ + ``` -在建议中,“indirection” 意味着不同于直接储存一个值,应该间接的储存一个指向值的指针。 +在建议中,_indirection_ 意味着不同于直接储存一个值,应该间接地存储一个指向值的指针。 -因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 `Box` 放入 `Cons` 成员中而不是直接存放另一个 `List` 值。`Box` 会指向另一个位于堆上的 `List` 值,而不是存放在 `Cons` 成员中。从概念上讲,我们仍然有一个通过在其中 “存放” 其他列表创建的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 +因为 `Box` 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。这意味着可以将 `Box` 放入 `Cons` 变体中而不是直接存放另一个 `List` 值。`Box` 会指向下一个位于堆上的 `List` 值,而不是存放在 `Cons` 变体中。从概念上讲,我们仍然是在创建一个包含其他列表的列表,不过现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。 我们可以修改示例 15-2 中 `List` 枚举的定义和示例 15-3 中对 `List` 的应用,如示例 15-5 所示,这是可以编译的: @@ -127,14 +126,14 @@ help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` repre 示例 15-5:为了拥有已知大小而使用 `Box` 的 `List` 定义 -`Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-2 展示了现在 `Cons` 成员看起来像什么: +`Cons` 变体将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 变体不储存值,所以它比 `Cons` 变体需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-2 展示了现在 `Cons` 变体看起来像什么: A finite Cons 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。 +`Box` 类型是一个智能指针,因为它实现了 `Deref` trait,它允许 `Box` 值被当作引用对待。当 `Box` 值离开作用域时,由于 `Box` 类型 `Drop` trait 的实现,box 所指向的堆数据也会被清除。这两个 trait 对于在本章余下讨论的其他智能指针所提供的功能中,将会更为重要。让我们更详细地探索一下这两个 trait。 [trait-objects]: ch18-02-trait-objects.html#顾及不同类型值的-trait-对象 diff --git a/src/ch15-02-deref.md b/src/ch15-02-deref.md index 99f2067..e0b9702 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -1,12 +1,11 @@ -## 通过 `Deref` trait 将智能指针当作常规引用处理 +## 使用 `Deref` Trait 将智能指针当作常规引用处理 -> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/main/src/ch15-02-deref.md) ->
-> commit 0514b1cf34c2eaab8285f43305c10a87f4ce34a0 + + -实现 `Deref` trait 允许我们重载 **解引用运算符**(_dereference operator_)`*`(不要与乘法运算符或通配符相混淆)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 +实现 `Deref` trait 允许我们定制**解引用运算符**(_dereference operator_)`*`(不要与乘法运算符或通配符相混淆)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并同样适用于智能指针。 -让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **Deref 强制转换**(_deref coercions_)功能以及它是如何处理引用或智能指针的。 +让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **Deref 强制转换**(_deref coercions_)特性以及它是如何处理引用或智能指针的。 > 我们将要构建的 `MyBox` 类型与真正的 `Box` 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 `Deref`,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。 @@ -22,7 +21,7 @@ 示例 15-6:使用解引用运算符来跟踪 `i32` 值的引用 -变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**),这样编译器就可以比较实际的值了。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 +变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是**解引用**),这样编译器就可以比较实际的值了。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 相反如果尝试编写 `assert_eq!(5, y);`,则会得到如下编译错误: @@ -72,15 +71,15 @@ 示例 15-9:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` -得到的编译错误是: +下面是相应的编译错误: ```console {{#include ../listings/ch15-smart-pointers/listing-15-09/output.txt}} ``` -`MyBox` 类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用 `*` 运算符的解引用功能,需要实现 `Deref` trait。 +`MyBox` 类型不能解引用,因为我们尚未在该类型上实现这个功能。为了启用 `*` 运算符的解引用功能,需要实现 `Deref` trait。 -### 通过实现 `Deref` trait 将某类型像引用一样处理 +### 实现 `Deref` trait 如第十章 [“为类型实现 trait”][impl-trait] 部分所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现: @@ -92,7 +91,7 @@ 示例 15-10:`MyBox` 上的 `Deref` 实现 -`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第二十章会详细介绍。 +`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多地担心它;第二十章会详细介绍。 `deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。回忆一下第五章 [“使用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分 `.0` 用来访问元组结构体的第一个元素。示例 15-9 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! @@ -106,7 +105,7 @@ Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。 -`deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 +`deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-9 中 `assert_eq!` 的 `5` 相匹配。 @@ -150,7 +149,7 @@ Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时 `(*m)` 将 `MyBox` 解引用为 `String`。接着 `&` 和 `[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有 Deref 强制转换所有这些符号混在一起将更难以读写和理解。Deref 强制转换使得 Rust 自动的帮我们处理这些转换。 -当所涉及到的类型定义了 `Deref` trait,Rust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗! +当所涉及到的类型定义了 `Deref` trait,Rust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时开销! ### Deref 强制转换如何与可变性交互 @@ -158,13 +157,13 @@ Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时 Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换: -* 当 `T: Deref` 时从 `&T` 到 `&U`。 -* 当 `T: DerefMut` 时从 `&mut T` 到 `&mut U`。 -* 当 `T: Deref` 时从 `&mut T` 到 `&U`。 +1. 当 `T: Deref` 时从 `&T` 到 `&U`。 +2. 当 `T: DerefMut` 时从 `&mut T` 到 `&mut U`。 +3. 当 `T: Deref` 时从 `&mut T` 到 `&U`。 -头两个情况除了第二种实现了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以直接得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。 +头两个情况除了第二种实现了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以透明地得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。 -第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 +第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但反之是**不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 [impl-trait]: ch10-02-traits.html#为类型实现-trait [tuple-structs]: ch05-01-defining-structs.html#使用没有命名字段的元组结构体来创建不同的类型 diff --git a/src/ch15-03-drop.md b/src/ch15-03-drop.md index c2dccf0..3a97d6b 100644 --- a/src/ch15-03-drop.md +++ b/src/ch15-03-drop.md @@ -1,18 +1,17 @@ ## 使用 `Drop` Trait 运行清理代码 -> [ch15-03-drop.md](https://github.com/rust-lang/book/blob/main/src/ch15-03-drop.md) ->
-> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e + + -对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。 +对于智能指针模式来说第二个重要的 trait 是 `Drop`,其允许我们在值要离开作用域时自定义要执行的操作。可以为任何类型提供 `Drop` trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。 -我们在智能指针上下文中讨论 `Drop` 是因为其功能几乎总是用于实现智能指针。例如,当 `Box` 被丢弃时会释放 box 指向的堆空间。 +我们在智能指针上下文中讨论 `Drop`,是因为在实现智能指针时几乎总会用到 `Drop` trait。例如,当 `Box` 被丢弃时会释放 box 指向的堆空间。 -在其他一些语言中的某些类型,我们不得不记住在每次使用完那些类型的智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源。 +在其他一些语言中的某些类型,我们不得不记住在每次使用完那些类型的智能指针实例后调用清理内存或资源的代码。常见示例包括文件句柄(file handles)、套接字(sockets)和锁(locks)。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码 —— 而且还不会泄漏资源! 指定在值离开作用域时应该执行的代码的方式是实现 `Drop` trait。`Drop` trait 要求实现一个叫做 `drop` 的方法,它获取一个 `self` 的可变引用。为了能够看出 Rust 何时调用 `drop`,让我们暂时使用 `println!` 语句实现 `drop`。 -示例 15-14 展示了唯一定制功能就是当其实例离开作用域时,打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`,这会演示 Rust 何时运行 `drop` 函数: +示例 15-14 展示了唯一定制功能就是当其实例离开作用域时,打印出 `Dropping CustomSmartPointer!` 的结构体 `CustomSmartPointer`,这会演示 Rust 何时运行 `drop` 方法: 文件名:src/main.rs @@ -22,7 +21,7 @@ 示例 15-14:结构体 `CustomSmartPointer`,其实现了放置清理代码的 `Drop` trait -`Drop` trait 包含在 prelude 中,所以无需导入它。我们在 `CustomSmartPointer` 上实现了 `Drop` trait,并提供了一个调用 `println!` 的 `drop` 方法实现。`drop` 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以可视化地展示 Rust 何时调用 `drop`。 +`Drop` trait 包含在 prelude 中,因此无需将其引入作用域。我们在 `CustomSmartPointer` 上实现了 `Drop` trait,并提供了一个调用 `println!` 的 `drop` 方法实现。`drop` 函数体是放置任何当类型实例离开作用域时期望运行的逻辑的地方。这里选择打印一些文本以可视化地展示 Rust 何时调用 `drop`。 在 `main` 中,我们新建了两个 `CustomSmartPointer` 实例并打印出了 `CustomSmartPointer created.`。在 `main` 的结尾,`CustomSmartPointer` 的实例会离开作用域,而 Rust 会调用放置于 `drop` 方法中的代码,打印出最后的信息。注意无需显式调用 `drop` 方法: @@ -34,9 +33,7 @@ 当实例离开作用域 Rust 会自动调用 `drop`,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃,所以 `d` 在 `c` 之前被丢弃。这个例子的作用是给了我们一个 drop 方法如何工作的可视化指导,不过通常需要指定类型所需执行的清理代码而不是打印信息。 -#### 通过 `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 那样的编译错误: @@ -54,13 +51,13 @@ {{#include ../listings/ch15-smart-pointers/listing-15-15/output.txt}} ``` -错误信息表明不允许显式调用 `drop`。错误信息使用了术语 **析构函数**(_destructor_),这是一个清理实例的函数的通用编程概念。**析构函数** 对应创建实例的 **构造函数**。Rust 中的 `drop` 函数就是这么一个析构函数。 +错误信息表明不允许显式调用 `drop`。错误信息使用了术语**析构函数**(_destructor_),这是一个清理实例的函数的通用编程术语。**析构函数** 对应创建实例的**构造函数**(_constructor_)。Rust 中的 `drop` 函数就是这么一个析构函数。 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 所示: +`std::mem::drop` 函数不同于 `Drop` trait 中的 `drop` 方法。可以通过传递希望强制丢弃的值作为参数。该函数位于 prelude,所以我们可以修改示例 15-15 中的 `main` 来调用 `drop` 函数。如示例 15-16 所示: 文件名:src/main.rs @@ -76,10 +73,10 @@ Rust 不允许我们显式调用 `drop` 因为 Rust 仍然会在 `main` 的结 {{#include ../listings/ch15-smart-pointers/listing-15-16/output.txt}} ``` -`` Dropping CustomSmartPointer with data `some data`! `` 出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,表明了 `drop` 方法被调用了并在此丢弃了 `c`。 +``Dropping CustomSmartPointer with data `some data`!`` 出现在 `CustomSmartPointer created.` 和 `CustomSmartPointer dropped before the end of main.` 之间,表明了 `drop` 方法被调用了并在此丢弃了 `c`。 -`Drop` trait 实现中指定的代码可以用于许多方面,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 `Drop` trait 和 Rust 所有权系统,你无需担心之后的代码清理,Rust 会自动考虑这些问题。 +`Drop` trait 实现中指定的代码可以用于多种场景,来使得清理变得方便和安全:比如可以用其创建我们自己的内存分配器!通过 `Drop` trait 和 Rust 所有权系统,你无需担心之后的代码清理,因为 Rust 会自动完成这些工作。 -我们也无需担心意外的清理掉仍在使用的值,这会造成编译器错误:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。 +你也不必担心由于不小心清理仍在使用的值而导致的问题:所有权系统确保引用总是有效的,也会确保 `drop` 只会在值不再被使用时被调用一次。 现在我们学习了 `Box` 和一些智能指针的特性,让我们聊聊标准库中定义的其他几种智能指针。 diff --git a/src/ch15-04-rc.md b/src/ch15-04-rc.md index 06a23b7..8e3e363 100644 --- a/src/ch15-04-rc.md +++ b/src/ch15-04-rc.md @@ -1,18 +1,17 @@ ## `Rc` 引用计数智能指针 -> [ch15-04-rc.md](https://github.com/rust-lang/book/blob/main/src/ch15-04-rc.md) ->
-> commit 52fafaaa8e432e84beaaf4ea80ccba880624effd + + 大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点在没有任何边指向它从而没有任何所有者之前,都不应该被清理掉。 -为了启用多所有权需要显式地使用 Rust 类型 `Rc`,其为 **引用计数**(_reference counting_)的缩写。引用计数意味着记录一个值的引用数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。 +为了启用多所有权需要显式地使用 Rust 类型 `Rc`,其为**引用计数**(_reference counting_)的缩写。引用计数意味着记录一个值的引用数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。 可以将其想象为客厅中的电视。当一个人进来看电视时,他打开电视。其他人也可以进来看电视。当最后一个人离开房间时,他关掉电视因为它不再被使用了。如果某人在其他人还在看的时候就关掉了电视,正在看电视的人肯定会抓狂的! `Rc` 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。 -注意 `Rc` 只能用于单线程场景;第十六章并发会涉及到如何在多线程程序中进行引用计数。 +注意 `Rc` 只能用于单线程场景;在第十六章讨论并发时会涉及到如何在多线程程序中进行引用计数。 ### 使用 `Rc` 共享数据 @@ -22,7 +21,7 @@ 图 15-3: 两个列表,`b` 和 `c`, 共享第三个列表 `a` 的所有权 -列表 `a` 包含 5 之后是 10,之后是另两个列表:`b` 从 3 开始而 `c` 从 4 开始。`b` 和 `c` 会接上包含 5 和 10 的列表 `a`。换句话说,这两个列表会尝试共享第一个列表所包含的 5 和 10。 +列表 `a` 包含 `5` 之后是 `10`,之后是另两个列表:`b` 从 `3` 开始而 `c` 从 `4` 开始。`b` 和 `c` 会接上包含 `5` 和 `10` 的列表 `a`。换句话说,这两个列表会尝试共享第一个列表所包含的 `5` 和 `10`。 尝试使用 `Box` 定义的 `List` 实现并不能工作,如示例 15-17 所示: @@ -44,7 +43,7 @@ 可以改变 `Cons` 的定义来存放一个引用,不过接着必须指定生命周期参数。通过指定生命周期参数,表明列表中的每一个元素都至少与列表本身存在的一样久。这是示例 15-17 中元素与列表的情况,但并不是所有情况都如此。 -相反,我们修改 `List` 的定义为使用 `Rc` 代替 `Box`,如列表 15-18 所示。现在每一个 `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 @@ -56,7 +55,7 @@ 需要使用 `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` 进行引用计数,可以明显的区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 `Rc::clone` 调用。 +也可以调用 `a.clone()` 而不是 `Rc::clone(&a)`,不过在这里 Rust 的习惯是使用 `Rc::clone`。`Rc::clone` 的实现并不像大部分类型的 `clone` 实现那样对所有数据进行深拷贝。`Rc::clone` 只会增加引用计数,这并不会花费多少时间。深拷贝可能会花费很长时间。通过使用 `Rc::clone` 进行引用计数,可以在视觉上区别深拷贝类的克隆和增加引用计数类的克隆。当查找代码中的性能问题时,只需考虑深拷贝类的克隆而无需考虑 `Rc::clone` 调用。 ### 克隆 `Rc` 会增加引用计数 @@ -72,7 +71,7 @@ 示例 15-19:打印出引用计数 -在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。这个函数叫做 `strong_count` 而不是 `count` 是因为 `Rc` 也有 `weak_count`;在 [“避免引用循环:将 `Rc` 变为 `Weak`”](ch15-06-reference-cycles.html#避免引用循环将-rct-变为-weakt) 部分会讲解 `weak_count` 的用途。 +在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 `Rc::strong_count` 函数获得。这个函数叫做 `strong_count` 而不是 `count` 是因为 `Rc` 也有 `weak_count`;在[“避免引用循环:将 `Rc` 变为 `Weak`”](ch15-06-reference-cycles.html#使用-weakt-防止引用循环)部分会讲解 `weak_count` 的用途。 这段代码会打印出: @@ -86,4 +85,4 @@ 通过不可变引用, `Rc` 允许在程序的多个部分之间只读地共享数据。如果 `Rc` 也允许多个可变引用,则会违反第四章讨论的借用规则之一:相同位置的多个可变借用可能造成数据竞争和不一致。不过可以修改数据是非常有用的!在下一部分,我们将讨论内部可变性模式和 `RefCell` 类型,它可以与 `Rc` 结合使用来处理不可变性的限制。 -[preventing-ref-cycles]: ch15-06-reference-cycles.html#避免引用循环将-rct-变为-weakt +[preventing-ref-cycles]: ch15-06-reference-cycles.html#使用-weakt-防止引用循环 diff --git a/src/ch15-05-interior-mutability.md b/src/ch15-05-interior-mutability.md index bd05541..1fc374a 100644 --- a/src/ch15-05-interior-mutability.md +++ b/src/ch15-05-interior-mutability.md @@ -1,8 +1,7 @@ ## `RefCell` 和内部可变性模式 -> [ch15-05-interior-mutability.md](https://github.com/rust-lang/book/blob/main/src/ch15-05-interior-mutability.md) ->
-> commit 5a3a64d60b0dd786c35ca4daada7a4d20da33e5e + + **内部可变性**(_Interior mutability_)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 `unsafe` 代码来模糊 Rust 通常的可变性和借用规则。不安全代码表明我们在手动检查这些规则而不是让编译器替我们检查。第二十章会更详细地介绍不安全代码。 @@ -10,30 +9,30 @@ 让我们通过遵循内部可变性模式的 `RefCell` 类型来开始探索。 -### 通过 `RefCell` 在运行时检查借用规则 +### 通过 `RefCell` 在运行时强制借用规则 不同于 `Rc`,`RefCell` 代表其数据的唯一的所有权。那么是什么让 `RefCell` 不同于像 `Box` 这样的类型呢?回忆一下第四章所学的借用规则: -1. 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 **之一**(而不是两者)。 -2. 引用必须总是有效的。 +- 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用**之一**(而不是两者)。 +- 引用必须始终有效。 -对于引用和 `Box`,借用规则的不可变性作用于编译时。对于 `RefCell`,这些不可变性作用于 **运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于 `RefCell`,如果违反这些规则程序会 panic 并退出。 +对于引用和 `Box`,借用规则的不可变性(invariants)在编译时就会被强制执行。对于 `RefCell`,这些不可变性作用于**运行时**。对于引用,如果违反这些规则,会得到一个编译错误。而对于 `RefCell`,如果违反这些规则程序会 panic 并退出。 在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 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` 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。 -类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用`RefCell`,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用 `RefCell` 的功能。 +类似于 `Rc`,`RefCell` 只能用于单线程场景。如果尝试在多线程上下文中使用 `RefCell`,会得到一个编译错误。第十六章会介绍如何在多线程程序中实现 `RefCell` 的功能。 如下为选择 `Box`,`Rc` 或 `RefCell` 的理由: -- `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 有单一所有者。 -- `Box` 允许在编译时执行不可变或可变借用检查;`Rc`仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变或可变借用检查。 +- `Rc` 允许相同数据有多个所有者;`Box` 和 `RefCell` 则只有单一所有者。 +- `Box` 允许在编译时执行不可变或可变借用检查;`Rc` 仅允许在编译时执行不可变借用检查;`RefCell` 允许在运行时执行不可变或可变借用检查。 - 因为 `RefCell` 允许在运行时执行可变借用检查,所以我们可以在即便 `RefCell` 自身是不可变的情况下修改其内部的值。 -在不可变值内部改变值就是 **内部可变性** 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 +在不可变值内部改变值就是**内部可变性**(_interior mutability_)模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。 ### 内部可变性:不可变值的可变借用 @@ -57,7 +56,7 @@ 有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 **测试替身**(_test double_)。就像电影制作中的替身演员 (_stunt double_) 一样,替代演员完成高难度的场景。测试替身在运行测试时替代某个类型。**mock 对象** 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。 -虽然 Rust 中的对象与其他语言中的对象并不是一回事,Rust 也没有像其他语言那样在标准库中内建 mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。 +Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rust 也没有像其他语言那样在标准库中内建 mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。 如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。 @@ -85,15 +84,16 @@ 测试代码定义了一个 `MockMessenger` 结构体,其 `sent_messages` 字段为一个 `String` 值的 `Vec` 用来记录被告知发送的消息。我们还定义了一个关联函数 `new` 以便于新建从空消息列表开始的 `MockMessenger` 值。接着为 `MockMessenger` 实现 `Messenger` trait 这样就可以为 `LimitTracker` 提供一个 `MockMessenger`。在 `send` 方法的定义中,获取传入的消息作为参数并储存在 `MockMessenger` 的 `sent_messages` 列表中。 -在测试中,我们测试了当 `LimitTracker` 被告知将 `value` 设置为超过 `max` 值 75% 的某个值。首先新建一个 `MockMessenger`,其从空消息列表开始。接着新建一个 `LimitTracker` 并传递新建 `MockMessenger` 的引用和 `max` 值 100。我们使用值 80 调用 `LimitTracker` 的 `set_value` 方法,这超过了 100 的 75%。接着断言 `MockMessenger` 中记录的消息列表应该有一条消息。 +在测试中,我们测试了当 `LimitTracker` 被告知将 `value` 设置为超过 `max` 值 75% 的某个值。首先新建一个 `MockMessenger`,其从空消息列表开始。接着新建一个 `LimitTracker` 并传递新建 `MockMessenger` 的引用和 `max` 值 `100`。我们使用值 `80` 调用 `LimitTracker` 的 `set_value` 方法,这超过了 100 的 75%。接着断言 `MockMessenger` 中记录的消息列表应该有一条消息。 + +然而,这个测试存在一个问题,如下所示: -然而,这个测试是有问题的: ```console {{#include ../listings/ch15-smart-pointers/listing-15-21/output.txt}} ``` -不能修改 `MockMessenger` 来记录消息,因为 `send` 方法获取了 `self` 的不可变引用。我们也不能参考错误文本的建议使用 `&mut self` 替代,因为这样 `send` 的签名就不符合 `Messenger` trait 定义中的签名了(可以试着这么改,看看会出现什么错误信息)。 +不能修改 `MockMessenger` 来记录消息,因为 `send` 方法接收的是对 `self` 的不可变引用。我们也不能采纳错误提示中将 `&self` 改为 `&mut self` 的建议,因为那样既要在 `impl` 方法中修改签名,也要在 `Messenger` trait 定义中修改签名。我们并不希望仅为了测试而改变 `Messenger` trait。相反,我们需要想办法让测试代码与现有设计兼容,正常工作。 这正是内部可变性的用武之地!我们将通过 `RefCell` 来储存 `sent_messages`,然后 `send` 将能够修改 `sent_messages` 并储存消息。示例 15-22 展示了代码: @@ -105,11 +105,11 @@ 示例 15-22:使用 `RefCell` 能够在外部值被认为是不可变的情况下修改内部值 -现在 `sent_messages` 字段的类型是 `RefCell>` 而不是 `Vec`。在 `new` 函数中新建了一个 `RefCell>` 实例替代空 vector。 +现在 `sent_messages` 字段的类型是 `RefCell>` 而不是 `Vec`。在 `new` 函数中在空 vector 外层创建了一个 `RefCell>` 实例。 -对于 `send` 方法的实现,第一个参数仍为 `self` 的不可变借用,这是符合方法定义的。我们调用 `self.sent_messages` 中 `RefCell` 的 `borrow_mut` 方法来获取 `RefCell` 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 `push` 以便记录测试过程中看到的消息。 +对于 `send` 方法的实现,第一个参数仍为 `self` 的不可变借用,这是符合 trait 定义的。我们调用 `self.sent_messages` 中 `RefCell>` 的 `borrow_mut` 方法来获取 `RefCell>` 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 `push` 以便记录测试过程中看到的消息。 -最后必须做出的修改位于断言中:为了看到其内部 vector 中有多少个项,需要调用 `RefCell` 的 `borrow` 以获取 vector 的不可变引用。 +最后必须做出的修改位于断言中:为了看到其内部 vector 中有多少个项,需要调用 `RefCell>` 的 `borrow` 以获取 vector 的不可变引用。 现在我们见识了如何使用 `RefCell`,让我们研究一下它怎样工作的! @@ -119,7 +119,7 @@ `RefCell` 记录当前有多少个活动的 `Ref` 和 `RefMut` 智能指针。每次调用 `borrow`,`RefCell` 将活动的不可变借用计数加一。当 `Ref` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell` 在任何时候只允许有多个不可变借用或一个可变借用。 -如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时出现 panic。示例 15-23 展示了对示例 15-22 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做: +如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell` 的实现会在运行时出现 panic。示例 15-23 展示了对示例 15-22 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell` 不允许我们在运行时这么做。 文件名:src/lib.rs @@ -127,7 +127,7 @@ {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-23/src/lib.rs:here}} ``` -示例 15-23:在同一作用域中创建两个可变引用并观察 `RefCell` panic +示例 15-23:在同一作用域中创建两个可变引用并观察 `RefCell` 将会 panic 这里为 `borrow_mut` 返回的 `RefMut` 智能指针创建了 `one_borrow` 变量。接着用相同的方式在变量 `two_borrow` 创建了另一个可变借用。这会在相同作用域中创建两个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败: @@ -141,7 +141,7 @@ ### 结合 `Rc` 和 `RefCell` 来拥有多个可变数据所有者 -`RefCell` 的一个常见用法是与 `Rc` 结合。回忆一下 `Rc` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell` 的 `Rc` 的话,就可以得到有多个所有者 **并且** 可以修改的值了! +`RefCell` 的一个常见用法是与 `Rc` 结合。回忆一下 `Rc` 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 `RefCell` 的 `Rc` 的话,就可以得到有多个所有者**并且**可以修改的值了! 例如,回忆示例 15-18 的 cons list 的例子中使用 `Rc` 使得多个列表共享另一个列表的所有权。因为 `Rc` 只存放不可变值,所以一旦创建了这些列表值后就不能修改。让我们加入 `RefCell` 来获得修改列表中值的能力。示例 15-24 展示了通过在 `Cons` 定义中使用 `RefCell`,我们就允许修改所有列表中的值了: @@ -153,18 +153,18 @@ 示例 15-24:使用 `Rc>` 创建可以修改的 `List` -这里创建了一个 `Rc>` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 成员创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。 +这里创建了一个 `Rc>` 实例并储存在变量 `value` 中以便之后直接访问。接着在 `a` 中用包含 `value` 的 `Cons` 变体创建了一个 `List`。需要克隆 `value` 以便 `a` 和 `value` 都能拥有其内部值 `5` 的所有权,而不是将所有权从 `value` 移动到 `a` 或者让 `a` 借用 `value`。 我们将列表 `a` 封装进了 `Rc` 这样当创建列表 `b` 和 `c` 时,它们都可以引用 `a`,正如示例 15-18 一样。 一旦创建了列表 `a`、`b` 和 `c`,我们将 `value` 的值加 10。为此对 `value` 调用了 `borrow_mut`,这里使用了第五章讨论的自动解引用功能([“`->` 运算符到哪去了?”][wheres-the---operator] 部分)来解引用 `Rc` 以获取其内部的 `RefCell` 值。`borrow_mut` 方法返回 `RefMut` 智能指针,可以对其使用解引用运算符并修改其内部值。 -当我们打印出 `a`、`b` 和 `c` 时,可以看到它们都拥有修改后的值 15 而不是 5: +当我们打印出 `a`、`b` 和 `c` 时,可以看到它们都拥有修改后的值 `15` 而不是 `5`: ```console {{#include ../listings/ch15-smart-pointers/listing-15-24/output.txt}} ``` -这是非常巧妙的!通过使用 `RefCell`,我们可以拥有一个表面上不可变的 `List`,不过可以使用 `RefCell` 中提供内部可变性的方法来在需要时修改数据。`RefCell` 的运行时借用规则检查也确实保护我们免于出现数据竞争——有时为了数据结构的灵活性而付出一些性能是值得的。注意 `RefCell` 不能用于多线程代码!`Mutex` 是一个线程安全版本的 `RefCell` ,我们会在第十六章讨论 `Mutex`。 +这个技巧非常巧妙!通过使用 `RefCell`,我们可以拥有一个表面上不可变的 `List`,不过可以使用 `RefCell` 中提供内部可变性的方法来在需要时修改数据。`RefCell` 的运行时借用规则检查也确实保护我们免于出现数据竞争,有时为了数据结构的灵活性而付出一些性能是值得的。注意 `RefCell` 不能用于多线程代码!`Mutex` 是一个线程安全版本的 `RefCell` ,我们会在第十六章讨论 `Mutex`。 [wheres-the---operator]: ch05-03-method-syntax.html#--运算符到哪去了 diff --git a/src/ch15-06-reference-cycles.md b/src/ch15-06-reference-cycles.md index 76a7e99..7747693 100644 --- a/src/ch15-06-reference-cycles.md +++ b/src/ch15-06-reference-cycles.md @@ -1,14 +1,13 @@ ## 引用循环与内存泄漏 -> [ch15-06-reference-cycles.md](https://github.com/rust-lang/book/blob/main/src/ch15-06-reference-cycles.md) ->
-> commit c06006157b14b3d47b5c716fc392b77f3b2e21ce + + -Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄漏**(_memory leak_)),但并不是不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。 +Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为 **内存泄漏**,_memory leak_),但并非不可能。Rust 并不保证完全防止内存泄漏,这意味着内存泄漏在 Rust 中被认为是内存安全的。这一点可以通过 `Rc` 和 `RefCell` 看出 Rust 允许出现内存泄漏:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,持有的数据也就永远不会被释放。 ### 制造引用循环 -让我们看看引用循环是如何发生的以及如何避免它。以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始: +让我们看看引用循环是如何发生的以及如何避免它,以示例 15-25 中的 `List` 枚举和 `tail` 方法的定义开始: 文件名:src/main.rs @@ -16,9 +15,9 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-25/src/main.rs}} ``` -示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 成员所引用的数据 +示例 15-25: 一个存放 `RefCell` 的 cons list 定义,这样可以修改 `Cons` 变体所引用的数据 -这里采用了示例 15-5 中 `List` 定义的另一种变体。现在 `Cons` 成员的第二个元素是 `RefCell>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 成员所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 成员的时候访问其第二项。 +这里采用了示例 15-5 中 `List` 定义的另一种变体。现在 `Cons` 变体的第二个元素是 `RefCell>`,这意味着不同于像示例 15-24 那样能够修改 `i32` 的值,我们希望能够修改 `Cons` 变体所指向的 `List`。这里还增加了一个 `tail` 方法来方便我们在有 `Cons` 变体的时候访问其第二项。 在示例 15-26 中增加了一个 `main` 函数,其使用了示例 15-25 中的定义。这些代码在 `a` 中创建了一个列表,一个指向 `a` 中列表的 `b` 列表,接着修改 `a` 中的列表指向 `b` 中的列表,这会创建一个引用循环。在这个过程的多个位置有 `println!` 语句展示引用计数。 @@ -30,9 +29,9 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 示例 15-26:创建一个引用循环:两个 `List` 值互相指向彼此 -这里在变量 `a` 中创建了一个 `Rc` 实例来存放初值为 `5, Nil` 的 `List` 值。接着在变量 `b` 中创建了存放包含值 10 和指向列表 `a` 的 `List` 的另一个 `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`。 +下来修改 `a` 使其指向 `b` 而不是 `Nil`,这就创建了一个循环。为此需要使用 `tail` 方法获取 `a` 中 `RefCell>` 的引用,并放入变量 `link` 中。接着使用 `RefCell>` 的 `borrow_mut` 方法将其值从存放 `Nil` 的 `Rc` 修改为 `b` 中的 `Rc`。 如果保持最后的 `println!` 行注释并运行代码,会得到如下输出: @@ -40,7 +39,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}} ``` -可以看到将列表 `a` 修改为指向 `b` 之后, `a` 和 `b` 中的 `Rc` 实例的引用计数都是 2。在 `main` 的结尾,Rust 丢弃 `b`,这会使 `b` `Rc` 实例的引用计数从 2 减为 1。然而,`b` `Rc` 不能被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 `a` 将 `a` `Rc` 实例的引用计数从 2 减为 1。这个实例也不能被回收,因为 `b` `Rc` 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更形象的展示,我们创建了一个如图 15-4 所示的引用循环: +可以看到将列表 `a` 修改为指向 `b` 之后, `a` 和 `b` 中的 `Rc` 实例的引用计数都是 2。在 `main` 的结尾,Rust 丢弃 `b`,这会使 `b` `Rc` 实例的引用计数从 2 减为 1。此时该 `Rc` 实例并不会被回收,因为其引用计数是 1 而不是 0。接下来 Rust 会丢弃 `a` 将 `a` `Rc` 实例的引用计数从 2 减为 1。这个实例也不能被回收,由于另一个 `Rc` 实例依然引用它,所以其引用计数是 1。这些列表的内存将永远保持未被回收的状态。为了更直观地展示这一引用循环,我们创建了一个如图 15-4 所示的示意图: Reference cycle of lists @@ -48,27 +47,25 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 如果取消最后 `println!` 的注释并运行程序,Rust 会尝试打印出 `a` 指向 `b` 指向 `a` 这样的循环直到栈溢出。 -相比真实世界的程序,这个例子中创建引用循环的结果并不可怕。创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。 +相比真实世界的程序,这个例子中创建引用循环的结果并不可怕:创建了引用循环之后程序立刻就结束了。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。 创建引用循环并不容易,但也不是不可能。如果你有包含 `Rc` 的 `RefCell` 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。创建引用循环是一个程序上的逻辑 bug,你应该使用自动化测试、代码评审和其他软件开发最佳实践来使其最小化。 -另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 `Cons` 成员拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。 +另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。在示例 15-25 中,我们总是希望 `Cons` 变体拥有其列表,所以重新组织数据结构是不可能的。让我们看看一个由父节点和子节点构成的图的例子,观察何时是使用无所有权的关系来避免引用循环的合适时机。 -### 避免引用循环:将 `Rc` 变为 `Weak` +### 使用 `Weak` 防止引用循环 -到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时才会被清理的 `Rc` 实例。你也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的 **弱引用**(_weak reference_)。强引用代表如何共享 `Rc` 实例的所有权。弱引用并不属于所有权关系,当 `Rc` 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。 +到目前为止,我们已经展示了调用 `Rc::clone` 会增加 `Rc` 实例的 `strong_count`,和只在其 `strong_count` 为 0 时 `Rc` 实例才会被清理。你也可以通过调用 `Rc::downgrade` 并传递 `Rc` 实例的引用来创建其值的**弱引用**(_weak reference_)。强引用代表如何共享 `Rc` 实例的所有权;弱引用不表达所有权关系,当 `Rc` 实例被清理时其计数没有影响。它们不会造成引用循环,因为任何涉及弱引用的循环会在其相关的值的强引用计数为 0 时被打断。 调用 `Rc::downgrade` 时会得到 `Weak` 类型的智能指针。不同于将 `Rc` 实例的 `strong_count` 加 1,调用 `Rc::downgrade` 会将 `weak_count` 加 1。`Rc` 类型使用 `weak_count` 来记录其存在多少个 `Weak` 引用,类似于 `strong_count`。其区别在于 `weak_count` 无需计数为 0 就能使 `Rc` 实例被清理。 -强引用代表如何共享 `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` 的情况,所以它不会返回非法指针。 - -我们会创建一个某项知道其子项和父项的树形结构的例子,而不是只知道其下一项的列表。 +作为示例,我们不再使用只知道下一个元素的列表,而是创建一个既知道子节点又知道父节点的树结构。 #### 创建树形数据结构:带有子节点的 `Node` -在最开始,我们将会构建一个带有子节点的树。让我们创建一个用于存放其拥有所有权的 `i32` 值和其子节点引用的 `Node`: +首先,我们将构建一个节点能够知道其子节点的树。创建一个用于存放其拥有所有权的 `i32` 值和对其子 `Node` 的引用: 文件名:src/main.rs @@ -78,7 +75,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 我们希望 `Node` 能够拥有其子节点,同时也希望能将所有权共享给变量,以便可以直接访问树中的每一个 `Node`,为此 `Vec` 的项的类型被定义为 `Rc`。我们还希望能修改其他节点的子节点,所以 `children` 中 `Vec>` 被放进了 `RefCell`。 -接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 3 且没有子节点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子节点的实例 `branch`,如示例 15-27 所示: +接下来,使用此结构体定义来创建一个叫做 `leaf` 的带有值 `3` 且没有子节点的 `Node` 实例,和另一个带有值 5 并以 `leaf` 作为子节点的实例 `branch`,如示例 15-27 所示: 文件名:src/main.rs @@ -88,7 +85,7 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 示例 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` 是其父节点。接下来我们会这么做。 #### 增加从子到父的引用 @@ -112,9 +109,9 @@ Rust 的内存安全性保证使其难以意外地制造永远也不会被清理 {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:there}} ``` -示例 15-28:一个 `leaf` 节点,其拥有指向其父节点 `branch` 的 `Weak` 引用 +示例 15-28:一个 `leaf` 节点,其拥有指向其父节点 `branch` 的弱引用 -创建 `leaf` 节点类似于示例 15-27,除了 `parent` 字段有所不同:`leaf` 开始时没有父节点,所以我们新建了一个空的 `Weak` 引用实例。 +创建 `leaf` 节点类似于示例 15-27,除了 `parent` 字段有所不同:`leaf` 开始时没有父节点,所以我们新建了一个空的 `Weak` 引用实例。 此时,当尝试使用 `upgrade` 方法获取 `leaf` 的父节点引用时,会得到一个 `None` 值。如第一个 `println!` 输出所示: @@ -152,13 +149,13 @@ children: RefCell { value: [] } }] } }) 如果在内部作用域结束后尝试访问 `leaf` 的父节点,会再次得到 `None`。在程序的结尾,`leaf` 中 `Rc` 的强引用计数为 1,弱引用计数为 0,因为现在 `leaf` 又是 `Rc` 唯一的引用了。 -所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子节点到父节点的关系为一个`Weak`引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。 +所有这些管理计数和值的逻辑都内建于 `Rc` 和 `Weak` 以及它们的 `Drop` trait 实现中。通过在 `Node` 定义中指定从子节点到父节点的关系为一个 `Weak` 引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。 ## 总结 -这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。`Box` 有一个已知的大小并指向分配在堆上的数据。`Rc` 记录了堆上数据的引用数量以便可以拥有多个所有者。`RefCell` 和其内部可变性提供了一个可以用于当需要不可变类型但是需要改变其内部值能力的类型,并在运行时而不是编译时检查借用规则。 +这一章涵盖了如何使用智能指针来做出不同于 Rust 常规引用默认所提供的保证与取舍。`Box` 有一个已知的大小并指向分配在堆上的数据。`Rc` 记录了堆上数据的引用计数从而允许多个所有者。`RefCell` 类型及其内部可变性允许我们在保持类型不可变的前提下更改其内部值;它也在运行时而非编译时执行借用规则检查。 -我们还介绍了提供了很多智能指针功能的 trait `Deref` 和 `Drop`。同时探索了会造成内存泄漏的引用循环,以及如何使用 `Weak` 来避免它们。 +我们还讨论了 trait `Deref` 和 `Drop`,它们实现了智能指针的许多功能。同时探索了会造成内存泄漏的引用循环,以及如何使用 `Weak` 来避免它们。 如果本章内容引起了你的兴趣并希望现在就实现你自己的智能指针的话,请阅读 [“The Rustonomicon”][nomicon] 来获取更多有用的信息。 diff --git a/src/ch20-02-advanced-traits.md b/src/ch20-02-advanced-traits.md index 6c0955f..6282c64 100644 --- a/src/ch20-02-advanced-traits.md +++ b/src/ch20-02-advanced-traits.md @@ -266,12 +266,12 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法 `Display` 的实现使用 `self.0` 来访问其内部的 `Vec`,因为 `Wrapper` 是元组结构体而 `Vec` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。 -这种做法的缺点在于因为 `Wrapper` 是一个新类型,它并不具备其所封装值的方法。必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上,这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。 +这种做法的缺点在于因为 `Wrapper` 是一个新类型,它并不具备其所封装值的方法。必须直接在 `Wrapper` 上实现 `Vec` 的所有方法,这样就可以代理到`self.0` 上,这就允许我们完全像 `Vec` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“使用 `Deref` Trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。 甚至当不涉及 trait 时 newtype 模式也很有用。现在让我们将关注点转移到一些与 Rust 类型系统交互的高级方式上来吧。 [newtype]: ch20-02-advanced-traits.html#使用-newtype-模式在外部类型上实现外部-trait [implementing-a-trait-on-a-type]: ch10-02-traits.html#为类型实现-trait [traits-defining-shared-behavior]: ch10-02-traits.html#trait定义共同行为 -[smart-pointer-deref]: ch15-02-deref.html#通过实现-deref-trait-将某类型像引用一样处理 +[smart-pointer-deref]: ch15-02-deref.html#使用-deref-trait-将智能指针当作常规引用处理 [tuple-structs]: ch05-01-defining-structs.html#使用没有命名字段的元组结构体来创建不同的类型