check ch15-02

This commit is contained in:
KaiserY 2017-09-19 10:23:33 +08:00
parent fe2455cb0d
commit de5b88965a
6 changed files with 165 additions and 117 deletions

View File

@ -4,44 +4,48 @@
> <br>
> commit 6e53771a409794d9933c2a31310d78149b7e0534
第十二章中,我们构建一个包含二进制 crate 和库 crate 的包。
第十二章中,我们构建一个包含二进制 crate 和库 crate 的包。你可能会发现,随着项目开发的深入,库 crate 持续增大,而你希望将其进一步拆分成多个库 crate。对于这种情况Cargo 提供了一个叫 **工作空间***workspaces*)的功能,它可以帮助我们管理多个相关的并行开发的包。
**工作空间** 是一系列共享同样的 *Cargo.lock* 和输出目录的包。让我们使用工作空间创建一个项目,这里采用常见的代码这样就可以关注工作空间的结构了。这里有一个使用了两个库的二进制项目:一个库会提供 `add_one` 方法而第二个会提供 `add_two` 方法。让我们以为这个二进制项目创建一个新 crate 作为开始:
不过如果库 crate 继续变得更大而我们想要进一步将包拆分为多个库 crate 呢随着包增长拆分出其主要组件将是非常有帮助的。对于这种情况Cargo 提供了一个叫**工作空间***workspaces*)的功能,它可以帮助我们管理多个相关的并行开发的包。
**工作空间**是一系列的包都共享同样的 *Cargo.lock* 和输出目录。让我们使用工作空间创建一个项目,这是我们熟悉的所以就可以关注工作空间的结构了。这里有一个二进制项目它使用了两个库:一个会提供`add_one`方法而第二个会提供`add_two`方法。让我们为这个二进制项目创建一个新 crate 作为开始:
```
```text
$ cargo new --bin adder
Created binary (application) `adder` project
$ cd adder
```
需要修改二进制包的 *Cargo.toml* 来告诉 Cargo 包`adder`是一个工作空间。再文件末尾增加如下
我们需要修改二进制包的 *Cargo.toml* 并增加一个 `[workspace]` 部分来告诉 Cargo 包 `adder` 是一个工作空间。在文件末尾增加如下内容
```toml
[workspace]
```
类似于很多 Cargo 的功能,工作空间支持配置惯例:只要遵循这些惯例就无需再增加任何配置了。这个惯例是任何作为子目录依赖的 crate 将是工作空间的一部分。让我们像这样在 *Cargo.toml* 中的`[dependencies]`增加一个`adder` crate 的路径依赖:
类似于很多 Cargo 的功能,工作空间支持配置惯例:只要遵循这些惯例就无需在 *Cargo.toml* 中增加更多的配置来定义工作空间了。
### 指定工作空间的依赖
工作空间惯例表明任何顶级 crate 依赖的位于任意子目录的 crate 都是工作空间的一部分。任何 crate无论是否在工作空间中可以在 *Cargo.toml* 中使用 `path` 属性来指定它拥有本地目录中的 crate 作为依赖。如果 crate 拥有 `[workspace]` 部分并指定了路径依赖,而这些路径是 crate 的子目录,则这些相关的 crate 被认为是工作空间的一部分。让我们在顶级的 `adder` crate 的 *Cargo.toml* 中为其指定位于 `add-one` 子目录的 `add-one` crate 作为依赖,通过这样修改 *Cargo.toml*
```toml
[dependencies]
add-one = { path = "add-one" }
```
如果增加依赖但没有指定`path`,这将是一个不位于工作空间的正常的依赖。
如果*Cargo.toml* 中增加依赖但没有指定对应 `path`,则这些将是不属于工作空间的假设来自于 Crates.io 的常规依赖。
接下来,在`adder`目录中生成`add-one` crate
### 在工作空间中创建第二个 crate
```
接下来,在 `adder` 目录中生成 `add-one` crate
```text
$ cargo new add-one
Created library `add-one` project
```
现在`adder`目录应该有如下目录和文件:
现在 `adder` 目录应该有如下目录和文件:
```
```text
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
@ -51,9 +55,9 @@ $ cargo new add-one
└── main.rs
```
*add-one/src/lib.rs* 中增加`add_one`函数的实现:
*add-one/src/lib.rs* 中增加 `add_one` 函数的实现:
<span class="filename">Filename: add-one/src/lib.rs</span>
<span class="filename">文件名: add-one/src/lib.rs</span>
```rust
pub fn add_one(x: i32) -> i32 {
@ -61,7 +65,7 @@ pub fn add_one(x: i32) -> i32 {
}
```
打开`adder`的 *src/main.rs* 并增加一行`extern crate`将新的`add-one`库引入作用域,并修改`main`函数来使用`add_one`函数
打开 `adder`*src/main.rs* 并增加一行 `extern crate` 将新的 `add-one` 库引入作用域,并修改 `main` 函数来调用 `add_one` 函数,如列表 14-12 所示
```rust,ignore
extern crate add_one;
@ -72,20 +76,40 @@ fn main() {
}
```
让我们构建一下!
<span class="caption">列表 14-12使用来自 `adder` crate 的库 crate `add-one`</span>
```
*adder* 目录下运行 `cargo build` 来构建 `adder` crate
```text
$ cargo build
Compiling add-one v0.1.0 (file:///projects/adder/add-one)
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.68 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.68 secs
```
注意*adder* 目录运行`cargo build`会构建这个 crate 和 *adder/add-one* 中的`add-one` crate不过只创建一个 *Cargo.lock* 和一个 *target* 目录,他们都位于 *adder* 目录。试试你能否用相同的方式增加`add-two` crate。
注意这会构建 `adder` crate 和 *adder/add-one* 中的 `add-one` crate。现在 *adder* 目录中应该有这些文件:
假如我们想要在`add-one` crate 中使用`rand` crate。一如既往在`Cargo.toml`的`[dependencies]`部分增加这个 crate
```text
├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── src
│ └── main.rs
└── target
```
<span class="filename">Filename: add-one/Cargo.toml</span>
工作空间在顶级目录有一个 *target* 目录;*add-one* 并没有自己的 *target* 目录。即使进入 `add-one` 目录运行 `cargo build`,构建结果也位于 *adder/target* 而不是 *adder/add-one/target*。因为工作空间中的 crate 之间相互依赖。如果每个 crate 有其自己的 *target* 目录,为了在自己的 *target* 目录中生成构建结果,工作空间中的每一个 crate 都不得不相互重新编译其他 crate。通过共享一个 *target* 目录,工作空间可以避免其他 crate 多余的重复构建。
#### 在工作空间中依赖外部 crate
还需注意的是工作空间只有一个 *Cargo.lock*,而不是拥有一个顶级的 *Cargo.lock* 和一个 *add-one/Cargo.lock*。这确保了所有的 crate 都使用完全相同版本的依赖。如果在 *Cargo.toml**add-one/Cargo.toml* 中都增加 `rand` crate则 Cargo 会将其都解析为同一版本并记录到唯一的 *Cargo.lock* 中。使得工作空间中的所有 crate 都使用相同的依赖意味着其中的 crate 都是相互减重的。现在就让我们来试一试。
让我们在 *add-one/Cargo.toml* 中的 `[dependencies]` 部分增加 `rand` crate 以便能够在 `add-one` crate 中使用 `rand` crate
<span class="filename">文件名: add-one/Cargo.toml</span>
```toml
[dependencies]
@ -93,9 +117,9 @@ $ cargo build
rand = "0.3.14"
```
如果在 *add-one/src/lib.rs* 中加上`extern crate rand;`后再运行`cargo build`,则会编译成功
现在就可以在 *add-one/src/lib.rs* 中增加 `extern crate rand;` 了,接着在 *adder* 目录运行 `cargo build` 构建整个工作空间就会引入并编译 `rand` crate
```
```text
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
@ -103,12 +127,12 @@ $ cargo build
Compiling rand v0.3.14
Compiling add-one v0.1.0 (file:///projects/adder/add-one)
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 10.18 secs
Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs
```
现在 *Cargo.lock* 的顶部反映了`add-one`依赖`rand`这一事实。然而即使在工作空间的某处使用了`rand`,也不能在工作空间的其他 crate 使用它,除非在对应的 *Cargo.toml* 也增加`rand`的依赖。例如,如果在顶层的`adder` crate 的 *src/main.rs* 中增加`extern crate rand;`,将会出现一个错误:
现在顶级的 *Cargo.lock* 包含了 `add-one``rand` 依赖的信息。然而,即使 `rand` 被用于工作空间的某处,也不能在其他 crate 中使用它,除非也在他们的 *Cargo.toml* 中加入 `rand`。例如,如果在顶级的 `adder` crate 的 *src/main.rs* 中增加 `extern crate rand;`,则会得到一个错误:
```
```text
$ cargo build
Compiling adder v0.1.0 (file:///projects/adder)
error[E0463]: can't find crate for `rand`
@ -118,11 +142,13 @@ error[E0463]: can't find crate for `rand`
| ^^^^^^^^^^^^^^^^^^^ can't find crate
```
为了修复这个错误,修改顶层的 *Cargo.toml* 并表明`rand`是`adder` crate 的一个依赖
为了修复这个错误,修改顶`adder` crate 的 *Cargo.toml* 来表明 `rand` 也是这个 crate 的依赖。构建 `adder` crate 会将 `rand` 加入到 *Cargo.lock*`adder` 的依赖列表中,但是这并不会下载 `rand` 的额外拷贝。Cargo 确保了工作空间中任何使用 `rand` 的 crate 都采用相同的版本。在整个工作空间中使用相同版本的 `rand` 节省了空间,因为这样就无需多个拷贝并确保了工作空间中的 crate 将是相互兼容的
作为另一个提高,为 crate 中的`add_one::add_one`函数增加一个测试:
#### 为工作空间增加测试
<span class="filename">Filename: add-one/src/lib.rs</span>
作为另一个提升,让我们为 `add_one` crate 中的 `add_one::add_one` 函数增加一个测试:
<span class="filename">文件名: add-one/src/lib.rs</span>
```rust
pub fn add_one(x: i32) -> i32 {
@ -140,12 +166,12 @@ mod tests {
}
```
现在在顶层的 *adder* 目录运行`cargo test`
在顶级 *adder* 目录运行 `cargo test`
```
```text
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running target/debug/adder-f0253159197f7841
running 0 tests
@ -153,25 +179,54 @@ running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
```
等等,零个测试?我们不是刚增加了一个吗?如果我们观察输出,就不难发现在工作空间中的`cargo test`只运行顶层 crate 的测试。为了运行其他 crate 的测试,需要使用`-p`参数来表明我们希望运行指定包的测试
等等,零个测试?我们刚刚增加了一个测试!如果我们观察输出,就不难发现在工作空间中的 `cargo test` 只运行顶级 crate 的测试。为了运行工作空间中所有 crate 的测试,需要使用 `--all` 参数
```
$ cargo test -p add-one
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
```text
$ cargo test --all
Finished dev [unoptimized + debuginfo] target(s) in 0.37 secs
Running target/debug/deps/add_one-abcabcabc
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/adder-abcabcabc
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
类似的,如果选择将工作空间发布到 crates.io其中的每一个包都需要单独发布。
当传递了 `--all` 时,`cargo test` 会运行工作空间中所有 crate 的测试。也可以选择在顶级目录运行工作空间中特定 crate 的测试,通过使用 `-p` 参数并指定希望测试的 crate 的名称:
```text
$ cargo test -p add-one
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/add_one-b3235fea9a156f74
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
输出显示了 `cargo test` 只运行了 `add-one` crate 的测试而没有运行 `adder` crate 的测试。
如果你选择向 crates.io 发布工作空间中的 crate每一个工作空间中的 crate 将会单独发布。`cargo publish` 命令并没有 `--all` 或者 `-p` 参数,所以必须进入每一个 crate 的目录并运行 `cargo publish` 来发布工作空间中的每一个 crate。
现在尝试以类似 `add-one` crate 的方式向工作空间增加 `add-two` crate 来作为更多的练习!
随着项目增长,考虑使用工作空间:每一个更小的组件比一大块代码要容易理解。将 crate 保持在工作空间中易于协调他们的改变,如果他们一起运行并经常需要同时被修改的话。

View File

@ -1,14 +1,16 @@
## 使用`cargo install`从 Crates.io 安装文件
## 使用 `cargo install` 从 Crates.io 安装二进制文件
> [ch14-04-installing-binaries.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-04-installing-binaries.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
> commit 6e53771a409794d9933c2a31310d78149b7e0534
`cargo install`命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有有二进制目标文件的包能够安装,而且所有二进制文件都被安装到 Rust 安装根目录的 *bin* 文件夹中。如果你使用 *rustup.rs* 安装的 Rust 且没有自定义任何配置,这将是`$HOME/.cargo/bin`。将这个目录添加到`$PATH`环境变量中就能够运行通过`cargo install`安装的程序了
`cargo install` 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。二进制目标文件是在 crate 有 *src/main.rs* 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标。通常 crate 的 *README* 文件中有该 crate 是库、二进制目标还是两者都是的信息
例如,第十二章提到的叫做`ripgrep`的用于搜索文件的`grep`的 Rust 实现。如果想要安装`ripgrep`,可以运行如下:
所有来自 `cargo install` 的二进制文件都安装到 Rust 安装根目录的 *bin* 文件夹中。如果你使用 *rustup.rs* 安装的 Rust 且没有自定义任何配置,这将是 `$HOME/.cargo/bin`。将这个目录添加到 `$PATH` 环境变量中就能够运行通过 `cargo install` 安装的程序了。
```
例如,第十二章提到的叫做 `ripgrep` 的用于搜索文件的 `grep` 的 Rust 实现。如果想要安装 `ripgrep`,可以运行如下:
```text
$ cargo install ripgrep
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading ripgrep v0.3.2
@ -18,4 +20,4 @@ Updating registry `https://github.com/rust-lang/crates.io-index`
Installing ~/.cargo/bin/rg
```
最后一行输出展示了安装的二进制文件的位置和名称,在这里`ripgrep`被命名为`rg`。只要你像上面提到的那样将安装目录假如`$PATH`,就可以运行`rg --help`并开始使用一个更快更 Rust 的工具来搜索文件了!
最后一行输出展示了安装的二进制文件的位置和名称,在这里 `ripgrep` 被命名为 `rg`。只要你像上面提到的那样将安装目录加入 `$PATH`,就可以运行 `rg --help` 并开始使用一个更快更 Rust 的工具来搜索文件了!

View File

@ -2,9 +2,9 @@
> [ch14-05-extending-cargo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-05-extending-cargo.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
> commit 6e53771a409794d9933c2a31310d78149b7e0534
Cargo 被设计为可扩展的,通过新的子命令而无须修改 Cargo 自身。如果`$PATH`中有类似`cargo-something`的二进制文件,就可以通过`cargo something`来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行`cargo --list`来展示出来,通过`cargo install`向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是很方便的
Cargo 被设计为可扩展的,通过新的子命令而无须修改 Cargo 自身。如果 `$PATH` 中有类似 `cargo-something` 的二进制文件,就可以通过 `cargo something` 来像 Cargo 子命令一样运行它。像这样的自定义命令也可以运行 `cargo --list` 来展示出来。能够通过 `cargo install` 向 Cargo 安装扩展并可以如内建 Cargo 工具那样运行他们是 Cargo 设计上的一个非常方便的优点
## 总结

View File

@ -2,17 +2,17 @@
> [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-00-smart-pointers.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
**指针**是一个常见的编程概念,它代表一个指向储存其他数据的位置。第四章学习了 Rust 的引用;他们是一类很平常的指针,以`&`符号为标志并借用了他们所指向的值。**智能指针***Smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和能,比如说引用计数。智能指针模式起源于 C++。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针**拥有**他们指向的数据。
**指针** *pointer*是一个常见的编程概念,它代表一个指向储存其他数据的位置。第四章学习了 Rust 的引用;他们是一类很平常的指针,以 `&` 符号为标志并借用了他们所指向的值。**智能指针***smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和能,比如说引用计数。智能指针模式起源于 C++。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 **拥有** 他们指向的数据。
本书中已经出现过一些智能指针,虽然当时我们并不这么称呼他们。例如在某种意义上说,第八章的`String`和`Vec<T>`都是智能指针。他们拥有一些数据并允许你修改他们,并带有元数据(比如他们的容量)和额外的功能或保证(`String`的数据总是有效的 UTF-8 编码)。智能指针区别于常规结构体的特性在于他们实现了`Deref`和`Drop` trait而本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。
本书中已经出现过一些智能指针,虽然当时我们并不这么称呼他们。例如在某种意义上说,第八章的 `String``Vec<T>` 都是智能指针。他们拥有一些数据并允许你修改他们,并带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。智能指针区别于常规结构体的显著特性在于他们实现了 `Deref` `Drop` trait而本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。
考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的。这里将会讲到的是来自标准库中最常用的一些:
* `Box<T>`,用于在堆上分配值
* `Rc<T>`,一个引用计数类型,其数据可以有多个所有者
* `RefCell<T>`,其本身并不是智能指针,不过它管理智能指针`Ref`和`RefMut`的访问,在运行时而不是在编译时执行借用规则。
* `RefCell<T>`,其本身并不是智能指针,不过它管理智能指针 `Ref` `RefMut` 的访问,在运行时而不是在编译时执行借用规则。
同时我们还将涉及:

View File

@ -1,12 +1,12 @@
## `Box<T>`用于已知大小的堆上数据
## `Box<T>` 用于已知大小的堆上数据
> [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-01-box.md)
> <br>
> commit 85b2c9ac704c9dc4bbedb97209d336afb9809dc1
> commit 348d78235faa10375ce5a3554e2c34d3275c174f
最简单直接的智能指针是 *box*,它的类型是`Box<T>`。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个`i32`
最简单直接的智能指针是 *box*,它的类型是 `Box<T>`。 box 允许你将一个值放在堆上(第四章介绍过栈与堆)。列表 15-1 展示了如何使用 box 在堆上储存一个`i32`
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
fn main() {
@ -15,14 +15,13 @@ fn main() {
}
```
<span class="caption">Listing 15-1: Storing an `i32` value on the heap using a
box</span>
<span class="caption">列表 15-1使用 box 在堆上储存一个 `i32`</span>
这会打印出`b = 5`。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像`b`这样的 box 在`main`的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
这会打印出 `b = 5`。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。
将一个单独的值存放在堆上并不是很有意义,所以像列表 15-1 这样单独使用 box 并不常见。一个 box 的实用场景是当你希望确保类型有一个已知大小的时候。例如,考虑一下列表 15-2它是一个用于 *cons list* 的枚举定义,这是一个来源于函数式编程的数据结构类型。注意它还不能编译:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
enum List {
@ -31,22 +30,21 @@ enum List {
}
```
<span class="caption">Listing 15-2: The first attempt of defining an enum to
represent a cons list data structure of `i32` values</span>
<span class="caption">列表 15-2第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举</span>
我们实现了一个只存放`i32`值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。
我们实现了一个只存放 `i32` 值的 cons list。也可以选择使用第十章介绍的泛型来实现一个类型无关的 cons list。
> #### cons list 的更多内容
>
> *cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons`函数("construct function"的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。
> *cons list* 是一个来源于 Lisp 编程语言及其方言的数据结构。在 Lisp 中,`cons` 函数“construct function" 的缩写)利用两个参数来构造一个新的列表,他们通常是一个单独的值和另一个列表。
>
> cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接”通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。
> cons 函数的概念涉及到更通用的函数式编程术语;“将 x 与 y 连接” 通常意味着构建一个新的容器而将 x 的元素放在新容器的开头,其后则是容器 y 的元素。
>
> cons list 通过递归调用`cons`函数产生。代表递归的 base case 的规范名称是`Nil`,它宣布列表的终止。注意这不同于第六章中的"null"或"nil"的概念,他们代表无效或缺失的值。
> cons list 通过递归调用 `cons` 函数产生。代表递归的终止条件base case的规范名称是 `Nil`,它宣布列表的终止。注意这不同于第六章中的 “null” 或 “nil” 的概念,他们代表无效或缺失的值。
cons list 是一个每个元素和之后的其余部分都只包含一个值的列表。列表的其余部分由嵌套的 cons list 定义。其结尾由值`Nil`表示。cons list 在 Rust 中并不常见;通常`Vec<T>`是一个更好的选择。实现这个数据结构是`Box<T>`实用性的一个好的例子。让我们看看为什么!
cons list 是一个每个元素和之后的其余部分都只包含一个值的列表。列表的其余部分由嵌套的 cons list 定义。其结尾由值 `Nil` 表示。cons list 在 Rust 中并不常见;通常 `Vec<T>` 是一个更好的选择。但是实现这个数据结构是 `Box<T>` 实用性的一个好的例子。让我们看看为什么!
使用 cons list 来储存列表`1, 2, 3`将看起来像这样:
使用 cons list 来储存列表 `1, 2, 3` 将看起来像这样:
```rust,ignore
use List::{Cons, Nil};
@ -56,29 +54,26 @@ fn main() {
}
```
第一个`Cons`储存了`1`和另一个`List`值。这个`List`是另一个包含`2`的`Cons`值和下一个`List`值。这又是另一个存放了`3`的`Cons`值和最后一个值为`Nil`的`List`,非递归成员代表了列表的结尾。
第一个 `Cons` 储存了 `1` 和另一个 `List` 值。这个 `List` 是另一个包含 `2` `Cons` 值和下一个 `List` 值。这又是另一个存放了 `3` `Cons` 值和最后一个值为 `Nil` `List`,非递归成员代表了列表的结尾。
如果尝试编译上面的代码,会得到如列表 15-3 所示的错误:
```
```text
error[E0072]: recursive type `List` has infinite size
-->
|
1 | enum List {
| _^ starting here...
2 | | Cons(i32, List),
3 | | Nil,
4 | | }
| |_^ ...ending here: recursive type has infinite size
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| --------------- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
```
<span class="caption">Listing 15-3: The error we get when attempting to define
a recursive enum</span>
<span class="caption">列表 15-3尝试定义一个递归枚举时得到的错误</span>
错误表明这个类型“有无限的大小”。为什么呢?因为`List`的一个成员被定义为递归的:它存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放`List`值到底需要多少空间。让我们一点一点看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。回忆一下第六章讨论枚举定义时的列表 6-2 中定义的`Message`枚举:
这个错误表明这个类型 “有无限的大小”。为什么呢?因为 `List` 的一个成员被定义为递归的:它存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。回忆一下第六章讨论枚举定义时的列表 6-2 中定义的 `Message` 枚举:
```rust
enum Message {
@ -89,14 +84,13 @@ enum Message {
}
```
当 Rust 需要知道需要为`Message`值分配多少空间时,它可以检查每一个成员并发现`Message::Quit`并不需要任何空间,`Message::Move`需要足够储存两个`i32`值的空间,依此类推。因此,`Message`值所需的最大空间等于储存其最大成员的空间大小。
当 Rust 需要知道需要为 `Message` 值分配多少空间时,它可以检查每一个成员并发现 `Message::Quit` 并不需要任何空间,`Message::Move` 需要足够储存两个 `i32` 值的空间,依此类推。因此,`Message` 值所需的空间等于储存其最大成员的空间大小。
与此相对当 Rust 编译器检查像列表 15-2 中的`List`这样的递归类型时会发生什么呢。编译器尝试计算出储存一个`List`枚举需要多少内存,并开始检查`Cons`成员,那么`Cons`需要的空间等于`i32`的大小加上`List`的大小。为了计算`List`需要多少内存,它检查其成员,从`Cons`成员开始。`Cons`成员储存了一个`i32`值和一个`List`值,这样的计算将无限进行下去,如图 15-4 所示:
与此相对当 Rust 编译器检查像列表 15-2 中的 `List` 这样的递归类型时会发生什么呢。编译器尝试计算出储存一个 `List` 枚举需要多少内存,并开始检查 `Cons` 成员,那么 `Cons` 需要的空间等于 `i32` 的大小加上 `List` 的大小。为了计算 `List` 需要多少内存,它检查其成员,从 `Cons` 成员开始。`Cons`成员储存了一个 `i32` 值和一个`List`值,这样的计算将无限进行下去,如图 15-4 所示:
<img alt="An infinite Cons list" src="img/trpl15-01.svg" class="center" style="width: 50%;" />
<span class="caption">Figure 15-4: An infinite `List` consisting of infinite
`Cons` variants</span>
<span class="caption">图 15-4一个包含无限个 `Cons` 成员的无限 `List`</span>
Rust 无法计算出要为定义为递归的类型分配多少空间,所以编译器给出了列表 15-3 中的错误。这个错误也包括了有用的建议:
@ -105,9 +99,9 @@ Rust 无法计算出要为定义为递归的类型分配多少空间,所以编
make `List` representable
```
因为`Box<T>`是一个指针,我们总是知道它需要多少空间:指针需要一个`usize`大小的空间。这个`usize`的值将是堆数据的地址。而堆数据可以是任意大小,不过开始这个堆数据的地址总是能放进一个`usize`中。所以如果将列表 15-2 的定义修改为像这里列表 15-5 中的定义,并修改`main`函数为`Cons`成员中的值使用`Box::new`
因为 `Box<T>` 是一个指针,我们总是知道它需要多少空间:指针需要一个 `usize` 大小的空间。这个 `usize` 的值将是堆数据的地址。而堆数据可以是任意大小,不过这个堆数据开头的地址总是能放进一个 `usize` 中。我们可以将列表 15-2 的定义修改为像这里列表 15-5 中的定义,并修改 `main` 函数对 `Cons` 成员中的值使用 `Box::new`
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
enum List {
@ -125,16 +119,14 @@ fn main() {
}
```
<span class="caption">Listing 15-5: Definition of `List` that uses `Box<T>` in
order to have a known size</span>
<span class="caption">列表 15-5为了已知大小使用 `Box<T>``List` 定义</span>
这样编译器就能够计算出储存一个`List`值需要的大小了。Rust 将会检查`List`,同样的从`Cons`成员开始检查。`Cons`成员需要`i32`的大小加上一个`usize`的大小,因为 box 总是`usize`大小的,不管它指向的是什么。接着 Rust 检查`Nil`成员,它并不储存一个值,所以`Nil`并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在`Cons`成员看起来像什么:
这样编译器就能够计算出储存一个 `List` 值需要的大小了。Rust 将会检查 `List`,同样的从 `Cons` 成员开始检查。`Cons` 成员需要 `i32` 的大小加上一个 `usize` 的大小,因为 box 总是 `usize` 大小的,不管它指向的是什么。接着 Rust 检查 `Nil` 成员,它并不储存一个值,所以 `Nil` 并不需要任何空间。我们通过 box 打破了这无限递归的连锁。图 15-6 展示了现在 `Cons` 成员看起来像什么:
<img alt="A finite Cons list" src="img/trpl15-02.svg" class="center" />
<span class="caption">Figure 15-6: A `List` that is not infinitely sized since
`Cons` holds a `Box`</span>
<span class="caption">图 15-6因为 `Cons` 存放一个 `Box` 所以 `List` 不是无限大小的了</span>
这就是 box 主要应用场景:打破无限循环的数据结构以便编译器可以知道其大小。第十七章讨论 trait 对象时我们将了解另一个 Rust 中会出现未知大小数据的情况。
虽然我们并不经常使用 box他们也是一个了解智能指针模式的好的方式。`Box<T>`作为智能指针经常被使用的两个方面是他们`Deref`和`Drop` trait 的实现。让我们研究这些 trait 如何工作以及智能指针如何利用他们。
虽然我们并不经常使用 box他们也是一个了解智能指针模式的好的方式。`Box<T>` 作为智能指针经常被使用的两个方面是他们 `Deref` `Drop` trait 的实现。让我们研究这些 trait 如何工作以及智能指针如何利用他们。

View File

@ -2,11 +2,11 @@
> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md)
> <br>
> commit ecc3adfe0cfa0a4a15a178dc002702fd0ea74b3f
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
第一个智能指针相关的重要 trait 是`Deref`,它允许我们重载`*`,解引用运算符(不同于乘法运算符和全局引用运算符)。重载智能指针的`*`能使访问其持有的数据更为方便,在本章结束前谈到解引用强制多态时我们会说明方便的意义
第一个智能指针相关的重要 trait 是 `Deref`,它允许我们重载 `*`,解引用运算符(不同于乘法运算符或全局引用运算符)。重载智能指针的 `*` 能使访问其持有的数据更为方便在本章结束前谈到解引用强制多态deref coercions时我们会说明方便意味着什么
第八章的哈希 map 的“根据旧值更新一个值”部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用`i32`值引用的例子:
第八章的哈希 map 的 “根据旧值更新一个值” 部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用 `i32` 值引用的例子:
```rust
let mut x = 5;
@ -19,13 +19,13 @@ let mut x = 5;
assert_eq!(6, x);
```
我们使用`*y`来访问可变引用`y`所指向的数据,而不是可变引用本身。接着可以修改它的数据,在这里对其加一。
我们使用 `*y` 来访问可变引用 `y` 所指向的数据,而不是可变引用本身。接着可以修改它的数据,在这里对其加一。
引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现`Deref` trait 来重载`*`运算符的行为。
引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据,因为解引用一个常规的引用只能给我们数据而不是元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现 `Deref` trait 来重载 `*` 运算符的行为。
列表 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过`Deref` trait 来重载`*`的例子。`Mp3`,在某种意义上是一个智能指针:它拥有包含音频的`Vec<u8>`数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现`Deref` trait 来返回音频数据。实现`Deref` trait 需要一个叫做`deref`的方法,它借用`self`并返回其内部数据:
列表 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过 `Deref` trait 来重载 `*` 的例子。`Mp3`,在某种意义上是一个智能指针:它拥有包含音频的 `Vec<u8>` 数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现 `Deref` trait 来返回音频数据。实现 `Deref` trait 需要一个叫做 `deref` 的方法,它借用 `self` 并返回其内部数据:
<span class="filename">Filename: src/main.rs</span>
<span class="filename">文件名: src/main.rs</span>
```rust
use std::ops::Deref;
@ -56,28 +56,27 @@ fn main() {
}
```
<span class="caption">Listing 15-7: An implementation of the `Deref` trait on a
struct that holds mp3 file data and metadata</span>
<span class="caption">列表 15-7一个存放 mp3 文件数据和元数据的结构体上的 `Deref` trait 实现</span>
大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的`type Item``type Target = T;`语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。
大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的 `type Item``type Target = T;` 语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。
在`assert_eq!`中,我们验证`vec![1, 2, 3]`是否为`Mp3`实例`*my_favorite_song`解引用的值,结果正是如此因为我们实现了`deref`方法来返回音频数据。如果没有为`Mp3`实现`Deref` traitRust 将不会编译`*my_favorite_song`:会出现错误说`Mp3`类型不能被解引用。
`assert_eq!` 中,我们验证 `vec![1, 2, 3]` 是否为 `Mp3` 实例 `*my_favorite_song` 解引用的值,结果正是如此因为我们实现了 `deref` 方法来返回音频数据。如果没有为 `Mp3` 实现 `Deref` traitRust 将不会编译 `*my_favorite_song`:会出现错误说 `Mp3` 类型不能被解引用。
没有`Deref` trait 的话,编译器只能解引用`&`引用,而`my_favorite_song`并不是(它是一个`Mp3`结构体)。通过`Deref` trait编译器知道实现了`Deref` trait 的类型有一个返回引用的`deref`方法(在这个例子中,是`&self.audio`因为列表 15-7 中的`deref`的定义)。所以为了得到一个`*`可以解引用的`&`引用,编译器将`*my_favorite_song`展开为如下:
没有 `Deref` trait 的话,编译器只能解引用 `&` 引用,而 `my_favorite_song` 并不是(它是一个 `Mp3` 结构体)。通过 `Deref` trait编译器知道实现了 `Deref` trait 的类型有一个返回引用的 `deref` 方法(在这个例子中,是 `&self.audio` 因为列表 15-7 中的 `deref` 的定义)。所以为了得到一个 `*` 可以解引用的 `&` 引用,编译器将 `*my_favorite_song` 展开为如下:
```rust,ignore
*(my_favorite_song.deref())
```
这个就是`self.audio`中的结果值。`deref`返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果`deref`方法直接返回值而不是引用,其值将被移动出`self`。和大部分使用解引用运算符的地方相同,这里并不想获取`my_favorite_song.audio`的所有权。
其结果就是 `self.audio` 中的值。`deref` 返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果 `deref` 方法直接返回值而不是引用,其值将被移动出 `self`。和大部分使用解引用运算符的地方相同,这里并不想获取 `my_favorite_song.audio` 的所有权。
注意将`*`替换为`deref`调用和`*`调用的过程在每次使用`*`的时候都会发生一次。`*`的替换并不会无限递归进行。最终的数据类型是`Vec<u8>`,它与列表 15-7 中`assert_eq!`的`vec![1, 2, 3]`相匹配。
注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `Vec<u8>`,它与列表 15-7 中 `assert_eq!` `vec![1, 2, 3]` 相匹配。
### 函数和方法的隐式解引用强制多态
Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的**解引用强制多态***deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多使用`&`和`*`的引用和解引用。
Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的 **解引用强制多态***deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多显式使用 `&` `*` 的引用和解引用。
使用列表 15-7 中的`Mp3`结构体,如下是一个获取`u8` slice 并压缩 mp3 音频数据的函数签名:
使用列表 15-7 中的 `Mp3` 结构体,如下是一个获取 `u8` slice 并压缩 mp3 音频数据的函数签名:
```rust,ignore
fn compress_mp3(audio: &[u8]) -> Vec<u8> {
@ -85,30 +84,30 @@ fn compress_mp3(audio: &[u8]) -> Vec<u8> {
}
```
如果 Rust 没有解引用强制多态,为了使用`my_favorite_song`中的音频数据调用此函数,必须写成:
如果 Rust 没有解引用强制多态,为了使用 `my_favorite_song` 中的音频数据调用此函数,必须写成:
```rust,ignore
compress_mp3(my_favorite_song.audio.as_slice())
```
也就是说,必须明确表用需要`my_favorite_song`中的`audio`字段而且我们希望有一个 slice 来引用这整个`Vec<u8>`。如果有很多地方需要用相同的方式处理`audio`数据,那么`.audio.as_slice()`就显得冗长重复了。
也就是说,必须明确表明需要 `my_favorite_song` 中的 `audio` 字段而且我们希望有一个 slice 来引用这整个 `Vec<u8>`。如果有很多地方需要用相同的方式处理 `audio` 数据,那么 `.audio.as_slice()` 就显得冗长重复了。
然而,因为解引用强制多态和`Mp3`的`Deref` trait 实现,我们可以使用如下代码使用`my_favorite_song`中的数据调用这个函数:
然而,因为解引用强制多态和 `Mp3` `Deref` trait 实现,我们可以使用如下代码使用 `my_favorite_song` 中的数据调用这个函数:
```rust,ignore
let result = compress_mp3(&my_favorite_song);
```
只有`&`和实例,好的!我们可以把智能指针当成普通的引用。也就是说解引用强制多态意味着 Rust 利用了`Deref`实现的优势Rust 知道`Mp3`实现了`Deref` trait 并从`deref`方法返回`&Vec<u8>`。它也知道标准库实现了`Vec<T>`的`Deref` trait其`deref`方法返回`&[T]`(我们也可以通过查阅`Vec<T>`的 API 文档来发现这一点。所以在编译时Rust 会发现它可以调用两次`Deref::deref`来将`&Mp3`变成`&Vec<u8>`再变成`&[T]`来满足`compress_mp3`的签名。这意味着我们可以少写一些代码Rust 会多次分析`Deref::deref`的返回值类型直到它满足参数的类型,只要相关类型实现了`Deref` trait。这些间接转换在编译时进行所以利用解引用强制多态并没有运行时惩罚
只有 `&` 和实例,好的!我们可以把智能指针当成普通的引用那样使用。也就是说解引用强制多态意味着 Rust 利用了 `Deref` 实现的优势Rust 知道 `Mp3` 实现了 `Deref` trait 并从 `deref` 方法返回 `&Vec<u8>`。它也知道标准库实现了 `Vec<T>``Deref` trait`deref` 方法返回 `&[T]`(我们也可以通过查阅 `Vec<T>` 的 API 文档来发现这一点。所以在编译时Rust 会发现它可以调用两次 `Deref::deref` 来将 `&Mp3` 变成 `&Vec<u8>` 再变成 `&[T]` 来满足 `compress_mp3` 的签名。这意味着我们可以少写一些代码Rust 会多次分析 `Deref::deref` 的返回值类型直到它满足参数的类型,只要相关类型实现了 `Deref` trait。这些间接转换在编译时进行所以利用解引用强制多态并没有运行时惩罚
类似于如何使用`Deref` trait 重载`&T`的`*`运算符,`DerefMut` trait用于重载`&mut T`的`*`运算符。
类似于如何使用 `Deref` trait 重载 `&T` `*` 运算符,`DerefMut` trait 用于重载 `&mut T` `*` 运算符。
Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
* 从`&T`到`&U`当`T: Deref<Target=U>`
* 从`&mut T`到`&mut U`当`T: DerefMut<Target=U>`
* 从`&mut T`到`&U`当`T: Deref<Target=U>`
* `T: Deref<Target=U>` 时从 `&T``&U`
* `T: DerefMut<Target=U>` 时从 `&mut T``&mut U`
* `T: Deref<Target=U>` 时从 `&mut T``&U`
头两个情况除了可变性之外是相同的:如果有一个`&T`,而`T`实现了返回`U`类型的`Deref`,可以直接得到`&U`。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是_不可能_的:不可变引用永远也不能强转为可变引用。
头两个情况除了可变性之外是相同的:如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`可以直接得到 `&U`。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是 **不可能** 的:不可变引用永远也不能强转为可变引用。
`Deref` trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新编写方法和函数来直接获取智能指针。
`Deref` trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新定义方法和函数来直接获取智能指针。