From d50fb8b05872ee5b554c99afbd767481b28ee91c Mon Sep 17 00:00:00 2001 From: KaiserY Date: Wed, 24 Jan 2018 16:56:28 +0800 Subject: [PATCH] check to ch15-02 --- src/SUMMARY.md | 2 +- src/ch14-04-installing-binaries.md | 8 +- src/ch14-05-extending-cargo.md | 6 +- src/ch15-00-smart-pointers.md | 59 +++++- src/ch15-01-box.md | 75 ++++++- src/ch15-02-deref.md | 329 ++++++++++++++++++++++++----- 6 files changed, 399 insertions(+), 80 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 2803635..449d2f4 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -82,7 +82,7 @@ - [智能指针](ch15-00-smart-pointers.md) - [`Box` 在堆上存储数据,并且可确定大小](ch15-01-box.md) - - [`Deref` Trait 允许通过引用访问数据](ch15-02-deref.md) + - [通过 `Deref` trait 将智能指针当作常规引用处理](ch15-02-deref.md) - [`Drop` Trait 运行清理代码](ch15-03-drop.md) - [`Rc` 引用计数智能指针](ch15-04-rc.md) - [`RefCell` 和内部可变性模式](ch15-05-interior-mutability.md) diff --git a/src/ch14-04-installing-binaries.md b/src/ch14-04-installing-binaries.md index 1e04b1e..9f956fe 100644 --- a/src/ch14-04-installing-binaries.md +++ b/src/ch14-04-installing-binaries.md @@ -2,11 +2,11 @@ > [ch14-04-installing-binaries.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-04-installing-binaries.md) >
-> commit 6e53771a409794d9933c2a31310d78149b7e0534 +> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf -`cargo install` 命令用于在本地安装和使用二进制 crate。它并不打算替换系统中的包;它意在作为一个方便 Rust 开发者们安装其他人已经在 crates.io 上共享的工具的手段。只有拥有二进制目标文件的包能够被安装。二进制目标文件是在 crate 有 *src/main.rs* 或者其他指定为二进制文件时所创建的可执行程序,这不同于自身不能执行但适合包含在其他程序中的库目标。通常 crate 的 *README* 文件中有该 crate 是库、二进制目标还是两者都是的信息。 +`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`,可以运行如下: @@ -14,7 +14,7 @@ $ cargo install ripgrep Updating registry `https://github.com/rust-lang/crates.io-index` Downloading ripgrep v0.3.2 - ...snip... + --snip-- Compiling ripgrep v0.3.2 Finished release [optimized + debuginfo] target(s) in 97.91 secs Installing ~/.cargo/bin/rg diff --git a/src/ch14-05-extending-cargo.md b/src/ch14-05-extending-cargo.md index d1ab126..26946b8 100644 --- a/src/ch14-05-extending-cargo.md +++ b/src/ch14-05-extending-cargo.md @@ -2,10 +2,10 @@ > [ch14-05-extending-cargo.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch14-05-extending-cargo.md) >
-> commit 6e53771a409794d9933c2a31310d78149b7e0534 +> commit ff93f82ff63ade5a352d9ccc430945d4ec804cdf -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 来分享代码是使得 Rust 生态环境可以用于许多不同的任务的重要组成部分。Rust 的标准库是小而稳定的,不过 crate 易于分享和使用,并采用一个不同语言自身的时间线来提供改进。不要羞于在 crates.io 上共享对你有用的代码;因为它很有可能对别人也很有用! \ No newline at end of file +通过 Cargo 和 [crates.io](https://crates.io) 来分享代码是使得 Rust 生态环境可以用于许多不同的任务的重要组成部分。Rust 的标准库是小而稳定的,不过 crate 易于分享和使用,并采用一个不同语言自身的时间线来提供改进。不要羞于在 [crates.io](https://crates.io) 上共享对你有用的代码;因为它很有可能对别人也很有用! \ No newline at end of file diff --git a/src/ch15-00-smart-pointers.md b/src/ch15-00-smart-pointers.md index 6d53dd5..cb11eb4 100644 --- a/src/ch15-00-smart-pointers.md +++ b/src/ch15-00-smart-pointers.md @@ -2,21 +2,64 @@ > [ch15-00-smart-pointers.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-00-smart-pointers.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 68267b982a226fa252e9afa1a5029396ccf5fa03 -**指针** (*pointer*)是一个常见的编程概念,它代表一个指向储存其他数据的位置。第四章学习了 Rust 的引用;他们是一类很平常的指针,以 `&` 符号为标志并借用了他们所指向的值。**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能,比如说引用计数。智能指针模式起源于 C++。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 **拥有** 他们指向的数据。 +**指针** (*pointer*)是一个包含内存地址的变量的通用概念。这个地址引用,或 “指向”(points at)一些其他数据。Rust 中最常见的指针是第四章介绍的 **引用**(*reference*)。引用以 `&` 符号为标志并借用了他们所指向的值。除了引用数据它们没有任何其他特殊功能。它们也没有任何额外开销,所以应用的最多。 -本书中已经出现过一些智能指针,虽然当时我们并不这么称呼他们。例如在某种意义上说,第八章的 `String` 和 `Vec` 都是智能指针。他们拥有一些数据并允许你修改他们,并带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。智能指针区别于常规结构体的显著特性在于他们实现了 `Deref` 和 `Drop` trait,而本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。 +另一方面,**智能指针**(*smart pointers*)是一类数据结构,他们的表现类似指针,但是也拥有额外的元数据和功能。智能指针的概念并不为 Rust 所独有;其起源于 C++ 并存在于其他语言中。Rust 标准库中不同的智能指针提供了多于引用的额外功能。本章将会探索的一个例子便是 **引用计数** (*reference counting*)智能指针类型,其允许数据有多个所有者。引用计数智能指针记录总共有多少个所有者,并当没有任何所有者时负责清理数据。 -考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的。这里将会讲到的是来自标准库中最常用的一些: + + + + + + +在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反大部分情况,智能指针 **拥有** 他们指向的数据。 + +实际上本书中已经出现过一些智能指针,比如第八章的 `String` 和 `Vec`,虽然当时我们并不这么称呼它们。这些类型都属于智能指针因为它们拥有一些数据并允许你修改它们。它们也带有元数据(比如他们的容量)和额外的功能或保证(`String` 的数据总是有效的 UTF-8 编码)。 + + + + +智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 `Deref` 和 `Drop` trait。`Deref` trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用又用于智能指针的代码。`Drop` trait 允许我们自定义当智能指针离开作用域时运行的代码。本章会讨论这些 trait 以及为什么对于智能指针来说他们很重要。 + +考虑到智能指针是一个在 Rust 经常被使用的通用设计模式,本章并不会覆盖所有现存的智能指针。很多库都有自己的智能指针而你也可以编写属于你自己的智能指针。这里将会讲到的是来自标准库中最常用的一些: + + + * `Box`,用于在堆上分配值 * `Rc`,一个引用计数类型,其数据可以有多个所有者 -* `RefCell`,其本身并不是智能指针,不过它管理智能指针 `Ref` 和 `RefMut` 的访问,在运行时而不是在编译时执行借用规则。 +* `Ref` 和 `RefMut`,通过 `RefCell` 访问,一个在运行时而不是在编译时执行借用规则的类型。 -同时我们还将涉及: + + -* **内部可变性**(*interior mutability*)模式,当一个不可变类型暴露出改变其内部值的 API,这时借用规则适用于运行时而不是编译时。 -* 引用循环,它如何会泄露内存,以及如何避免他们 +同时我们会涉及 **内部可变性**(*interior mutability*)模式,这时不可变类型暴露出改变其内部值的 API。我们也会讨论 **引用循环**(*reference cycles*)会如何泄露内存,以及如何避免。 让我们开始吧! diff --git a/src/ch15-01-box.md b/src/ch15-01-box.md index e7ecd8d..0c5703d 100644 --- a/src/ch15-01-box.md +++ b/src/ch15-01-box.md @@ -2,13 +2,18 @@ > [ch15-01-box.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-01-box.md) >
-> commit 68267b982a226fa252e9afa1a5029396ccf5fa03 +> commit 0905e41f7387b60865e6eac744e31a7f7b46edf5 -最简单直接的智能指针是 *box*,它的类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 +最简单直接的智能指针是 *box*,其类型是 `Box`。 box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。如果你想回顾一下栈与堆的区别请参考第四章。 + + + 除了数据被储存在堆上而不是栈上之外,box 没有性能损失,不过也没有很多额外的功能。他们多用于如下场景: -- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型的值的时候 +- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候 - 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候 - 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候 @@ -31,16 +36,40 @@ fn main() { 示例 15-1:使用 box 在堆上储存一个 `i32` 值 -这里定义了变量 `b`,其值是一个指向被分配在堆上的值 `5` 的 `Box`。这个程序会打印出 `b = 5`。在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。 +这里定义了变量 `b`,其值是一个指向被分配在堆上的值 `5` 的 `Box`。这个程序会打印出 `b = 5`;在这个例子中,我们可以像数据是储存在栈上的那样访问 box 中的数据。正如任何拥有数据所有权的值那样,当像 `b` 这样的 box 在 `main` 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。 -将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。将像单个 `i32` 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个 box 定义一个不使用 box 时无法定义的类型的例子。 +将一个单独的值存放在堆上并不是很有意义,所以像示例 15-1 这样单独使用 box 并不常见。将像单个 `i32` 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。让我们看看一个不使用 box 时无法定义的类型的例子。 + + + ### box 允许创建递归类型 + + + + + Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是 **递归类型**(*recursive type*),其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。 让我们探索一下 *cons list*,一个函数式编程语言中的常见类型,来展示这个(递归类型)概念。除了递归之外,我们将要定义的 cons list 类型是很直白的,所以这个例子中的概念在任何遇到更为复杂的涉及到递归类型的场景时都很实用。 + + + cons list 是一个每一项都包含两个部分的列表:当前项的值和下一项。其最后一项值包含一个叫做 `Nil` 的值并没有下一项。 > #### cons list 的更多内容 @@ -53,6 +82,15 @@ cons list 是一个每一项都包含两个部分的列表:当前项的值和 注意虽然函数式编程语言经常使用 cons list,但是它并不是一个 Rust 中常见的类型。大部分在 Rust 中需要列表的时候,`Vec` 是一个更好的选择。其他更为复杂的递归数据类型 **确实** 在 Rust 的很多场景中很有用,不过通过以 cons list 作为开始,我们可以探索如何使用 box 毫不费力的定义一个递归数据类型。 + + + 示例 15-2 包含一个 cons list 的枚举定义。注意这还不能编译因为这个类型没有已知的大小,之后我们会展示: 文件名: src/main.rs @@ -66,7 +104,13 @@ enum List { 示例 15-2:第一次尝试定义一个代表 `i32` 值的 cons list 数据结构的枚举 -> 注意:出于示例的我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型实现它,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 +> 注意:出于示例的需要我们选择实现一个只存放 `i32` 值的 cons list。也可以用泛型实现它,正如第十章讲到的,来定义一个可以存放任何类型值的 cons list 类型。 + + + 使用这个 cons list 来储存列表 `1, 2, 3` 将看起来如示例 15-3 所示: @@ -93,7 +137,7 @@ error[E0072]: recursive type `List` has infinite size 1 | enum List { | ^^^^^^^^^ recursive type has infinite size 2 | Cons(i32, List), - | --------------- recursive without indirection + | ----- recursive without indirection | = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable @@ -101,6 +145,14 @@ error[E0072]: recursive type `List` has infinite size 示例 15-4:尝试定义一个递归枚举时得到的错误 + + + 这个错误表明这个类型 “有无限的大小”。其原因是 `List` 的一个成员被定义为是递归的:它直接存放了另一个相同类型的值。这意味着 Rust 无法计算为了存放 `List` 值到底需要多少空间。让我们一点一点来看:首先了解一下 Rust 如何决定需要多少空间来存放一个非递归类型。 ### 计算非递归类型的大小 @@ -159,7 +211,7 @@ fn main() { } ``` -示例 15-6:为了已知大小而使用 `Box` 的 `List` 定义 +示例 15-6:为了拥有已知大小而使用 `Box` 的 `List` 定义 `Cons` 成员将会需要一个 `i32` 的大小加上储存 box 指针数据的空间。`Nil` 成员不储存值,所以它比 `Cons` 成员需要更少的空间。现在我们知道了任何 `List` 值最多需要一个 `i32` 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 `List` 值需要的大小了。图 15-7 展示了现在 `Cons` 成员看起来像什么: @@ -170,3 +222,10 @@ fn main() { 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 b405616..c0e0ef3 100644 --- a/src/ch15-02-deref.md +++ b/src/ch15-02-deref.md @@ -1,113 +1,330 @@ -## `Deref` Trait 允许通过引用访问数据 +## 通过 `Deref` trait 将智能指针当作常规引用处理 > [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md) >
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 -第一个智能指针相关的重要 trait 是 `Deref`,它允许我们重载 `*`,解引用运算符(不同于乘法运算符或全局引用运算符)。重载智能指针的 `*` 能使访问其持有的数据更为方便,在本章结束前谈到解引用强制多态(deref coercions)时我们会说明方便意味着什么。 +实现 `Deref` trait 允许我们重载 **解引用运算符**(*dereference operator*)`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。 -第八章的哈希 map 的 “根据旧值更新一个值” 部分简要的提到了解引用运算符。当时有一个可变引用,而我们希望改变这个引用所指向的值。为此,首先我们必须解引用。这是另一个使用 `i32` 值引用的例子: + + + + + + +让我们首先看看 `*` 如何处理引用,接着尝试定义我们自己的类 `Box` 类型并看看为何 `*` 不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态**(*deref coercions*)功能和它是如何一同处理引用或智能指针的。 + +### 通过 `*` 追钟指针的值 + + + + + + + +常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-8 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据: + + + + + + + +文件名: src/main.rs ```rust -let mut x = 5; -{ - let y = &mut x; +fn main() { + let x = 5; + let y = &x; - *y += 1 + assert_eq!(5, x); + assert_eq!(5, *y); } - -assert_eq!(6, x); ``` -我们使用 `*y` 来访问可变引用 `y` 所指向的数据,而不是可变引用本身。接着可以修改它的数据,在这里是对其加一。 +示例 15-8:使用解引用运算符来跟踪 `i32` 值的引用 -引用并不是智能指针,他们只是引用指向的一个值,所以这个解引用操作是很直接的。智能指针还会储存指针或数据的元数据。当解引用一个智能指针时,我们只想要数据,而不需要元数据,因为解引用一个常规的引用只能给我们数据而不是元数据。我们希望能在使用常规引用的地方也能使用智能指针。为此,可以通过实现 `Deref` trait 来重载 `*` 运算符的行为。 +变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。 -示例 15-7 展示了一个定义为储存 mp3 数据和元数据的结构体通过 `Deref` trait 来重载 `*` 的例子。`Mp3`,在某种意义上是一个智能指针:它拥有包含音频的 `Vec` 数据。另外,它储存了一些可选的元数据,在这个例子中是音频数据中艺术家和歌曲的名称。我们希望能够方便的访问音频数据而不是元数据,所以需要实现 `Deref` trait 来返回音频数据。实现 `Deref` trait 需要一个叫做 `deref` 的方法,它借用 `self` 并返回其内部数据: +相反如果尝试编写 `assert_eq!(5, y);`,则会得到如下编译错误: + +```text +error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is +not satisfied + --> src/main.rs:6:5 + | +6 | assert_eq!(5, y); + | ^^^^^^^^^^^^^^^^^ can't compare `{integer}` with `&{integer}` + | + = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for + `{integer}` +``` + +不允许比较数字的引用与数字,因为它们是不同的类型。必须使用 `*` 追踪引用所指向的值。 + +### 像引用一样使用 `Box` + +可以重写示例 15-8 中的代码来使用 `Box` 而不是引用,同时借引用运算符也一样能工作,如示例 15-9 所示: + +文件名: src/main.rs + +```rust +fn main() { + let x = 5; + let y = Box::new(x); + + assert_eq!(5, x); + assert_eq!(5, *y); +} +``` + +示例 15-9:在 `Box` 上使用解引用运算符 + +相比示例 15-8 唯一修改的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。让我们通过实现自己的 box 类型来探索 `Box` 能这么做有何特殊之处。 + +### 自定义智能指针 + +为了体会默认智能指针的行为不同于引用,让我们创建一个类似于标准库提供的 `Box` 类型的只能指针。接着会学习如何增加使用解引用运算符的功能。 + +从根本上说,`Box` 被定义为包含一个元素的元组结构体,所以示例 15-10 以相同的方式定义了 `MyBox` 类型。我们还定义了 `new` 函数来对应定义于 `Box` 的 `new` 函数: + +文件名: src/main.rs + +```rust +struct MyBox(T); + +impl MyBox { + fn new(x: T) -> MyBox { + MyBox(x) + } +} +``` + +示例 15-10:定义 `MyBox` 类型 + +这里定义了一个结构体 `MyBox` 并声明了一个泛型 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。 + +尝试将示例 15-9 中的代码加入示例 15-10 中并修改 `main` 使用我们定义的 `MyBox` 类型代替 `Box`。示例 15-11 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`: + +文件名: src/main.rs + +```rust,ignore +fn main() { + let x = 5; + let y = MyBox::new(x); + + assert_eq!(5, x); + assert_eq!(5, *y); +} +``` + +示例 15-11:尝试以使用引用和 `Box` 相同的方式使用 `MyBox` + +得到的编译错误是: + +```text +error: type `MyBox<{integer}>` cannot be dereferenced + --> src/main.rs:14:19 + | +14 | assert_eq!(5, *y); + | ^^ +``` + +`MyBox` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,可以实现 `Deref` trait。 + +### 实现 `Deref` trait 定义如何像引用一样对待某类型 + +如第十章所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-12 包含定义于 `MyBox` 之上的 `Deref` 实现: 文件名: src/main.rs ```rust use std::ops::Deref; -struct Mp3 { - audio: Vec, - artist: Option, - title: Option, -} +# struct MyBox(T); +impl Deref for MyBox { + type Target = T; -impl Deref for Mp3 { - type Target = Vec; - - fn deref(&self) -> &Vec { - &self.audio + fn deref(&self) -> &T { + &self.0 } } - -fn main() { - let my_favorite_song = Mp3 { - // we would read the actual audio data from an mp3 file - audio: vec![1, 2, 3], - artist: Some(String::from("Nirvana")), - title: Some(String::from("Smells Like Teen Spirit")), - }; - - assert_eq!(vec![1, 2, 3], *my_favorite_song); -} ``` -示例 15-7:一个存放 mp3 文件数据和元数据的结构体上的 `Deref` trait 实现 +示例 15-12:`MyBox` 上的 `Deref` 实现 -大部分代码看起来都比较熟悉:一个结构体、一个 trait 实现、和一个创建了结构体实例的 main 函数。其中有一部分我们还未全面的讲解:类似于第十三章学习迭代器 trait 时出现的 `type Item`,`type Target = T;` 语法用于定义关联类型,第十九章会更详细的介绍。不必过分担心例子中的这一部分;它只是一个稍显不同的定义泛型参数的方式。 +`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。 -在 `assert_eq!` 中,我们验证 `vec![1, 2, 3]` 是否为 `Mp3` 实例 `*my_favorite_song` 解引用的值,结果正是如此,因为我们实现了 `deref` 方法来返回音频数据。如果没有为 `Mp3` 实现 `Deref` trait,Rust 将不会编译 `*my_favorite_song`:会出现错误说 `Mp3` 类型不能被解引用。 + + -没有 `Deref` trait 的话,编译器只能解引用 `&` 引用,而 `my_favorite_song` 并不是(它是一个 `Mp3` 结构体)。通过 `Deref` trait,编译器知道实现了 `Deref` trait 的类型有一个返回引用的 `deref` 方法(在这个例子中,是 `&self.audio` 因为示例 15-7 中的 `deref` 的定义)。所以为了得到一个 `*` 可以解引用的 `&` 引用,编译器将 `*my_favorite_song` 展开为如下: +`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-11 中的 `main` 函数中对 `MyBox` 值的 `*` 调用现在可以编译并能通过断言了! + +没有 `Deref` trait 的话,编译器只能解引用 `&` 引用。`Deref` trait 的 `deref` 方法为编译器提供了获取任何实现了 `Deref` 的类型值的能力,为了获取其知道如何解引用的 `&` 引用编译器可以调用 `deref` 方法。 + +当我们在示例 15-11 中输入 `*y` 时,Rust 事实上在底层运行了如下代码: ```rust,ignore -*(my_favorite_song.deref()) +*(y.deref()) ``` -其结果就是 `self.audio` 中的值。`deref` 返回一个引用并接下来必需解引用而不是直接返回值的原因是所有权:如果 `deref` 方法直接返回值而不是引用,其值将被移动出 `self`。和大部分使用解引用运算符的地方相同,这里并不想获取 `my_favorite_song.audio` 的所有权。 + + + +Rust 将 `*` 运算符替换为 `deref` 方法调用和一个普通解引用,如此我们便无需担心是否需要调用 `deref` 方法。Rust 的这个功能让我们可以编写同时处理常规引用或实现了 `Deref` 的类型的代码。 + +`deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox` 内部值的所有权。 + +注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `i32`,它与示例 15-11 中 `assert_eq!` 的 `5` 相匹配。 -注意将 `*` 替换为 `deref` 调用和 `*` 调用的过程在每次使用 `*` 的时候都会发生一次。`*` 的替换并不会无限递归进行。最终的数据类型是 `Vec`,它与示例 15-7 中 `assert_eq!` 的 `vec![1, 2, 3]` 相匹配。 ### 函数和方法的隐式解引用强制多态 -Rust 倾向于偏爱明确而不是隐晦,不过一个情况下这并不成立,就是函数和方法的参数的 **解引用强制多态**(*deref coercions*)。解引用强制多态会自动的将指针或智能指针的引用转换为指针内容的引用。解引用强制多态发生于当传递给函数的参数类型不同于函数签名中定义参数类型的时候。解引用强制多态的加入使得 Rust 调用函数或方法时无需很多显式使用 `&` 和 `*` 的引用和解引用。 + + -使用示例 15-7 中的 `Mp3` 结构体,如下是一个获取 `u8` slice 并压缩 mp3 音频数据的函数签名: +**解引用强制多态**(*deref coercions*)是 Rust 出于方便的考虑作用于函数或方法的参数的。其将实现了 `Deref` 的类型的引用转换为 `Deref` 所能够将原始类型转换的类型的引用。解引用强制多态发生于当作为参数传递给函数或方法的特定类型的引用不同于函数或方法签名中定义参数类型的时候,这时会有一系列的 `deref` 方法调用会将提供的类型转换为参数所需的类型。 -```rust,ignore -fn compress_mp3(audio: &[u8]) -> Vec { - // the actual implementation would go here +解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&` 和 `*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。 + +作为展示解引用强制多态的实例,让我们使用示例 15-10 中定义的 `MyBox`,以及示例 15-12 中增加的 `Deref` 实现。示例 15-13 展示了一个有着字符串 slice 参数的函数定义: + +文件名: src/main.rs + +```rust +fn hello(name: &str) { + println!("Hello, {}!", name); } ``` -如果 Rust 没有解引用强制多态,为了使用 `my_favorite_song` 中的音频数据调用此函数,必须写成: +示例 15-13:`hello` 函数有着 `&str` 类型的参数 `name` -```rust,ignore -compress_mp3(my_favorite_song.audio.as_slice()) +可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox` 类型值的引用调用 `hello` 称为可能,如示例 15-14 所示: + +文件名: src/main.rs + +```rust +# use std::ops::Deref; +# +# struct MyBox(T); +# +# impl MyBox { +# fn new(x: T) -> MyBox { +# MyBox(x) +# } +# } +# +# impl Deref for MyBox { +# type Target = T; +# +# fn deref(&self) -> &T { +# &self.0 +# } +# } +# +# fn hello(name: &str) { +# println!("Hello, {}!", name); +# } +# +fn main() { + let m = MyBox::new(String::from("Rust")); + hello(&m); +} ``` -也就是说,必须明确表明需要 `my_favorite_song` 中的 `audio` 字段而且我们希望有一个 slice 来引用这整个 `Vec`。如果有很多地方需要用相同的方式处理 `audio` 数据,那么 `.audio.as_slice()` 就显得冗长重复了。 +示例 15-14:因为解引用强制多态,使用 `MyBox` 的引用调用 `hello` 是可行的 -然而,因为解引用强制多态和 `Mp3` 的 `Deref` trait 实现,我们可以使用如下代码使用 `my_favorite_song` 中的数据调用这个函数: +这里使用 `&m` 调用 `hello` 函数,其为 `MyBox` 值的引用。因为示例 15-12 中在 `MyBox` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。 -```rust,ignore -let result = compress_mp3(&my_favorite_song); +如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox` 类型的值调用 `hello`,则不得不编写示例 15-15 中的代码来代替示例 15-14: + +文件名: src/main.rs + +```rust +# use std::ops::Deref; +# +# struct MyBox(T); +# +# impl MyBox { +# fn new(x: T) -> MyBox { +# MyBox(x) +# } +# } +# +# impl Deref for MyBox { +# type Target = T; +# +# fn deref(&self) -> &T { +# &self.0 +# } +# } +# +# fn hello(name: &str) { +# println!("Hello, {}!", name); +# } +# +fn main() { + let m = MyBox::new(String::from("Rust")); + hello(&(*m)[..]); +} ``` -只有 `&` 和实例,好的!我们可以把智能指针当成普通的引用那样使用。也就是说解引用强制多态意味着 Rust 利用了 `Deref` 实现的优势:Rust 知道 `Mp3` 实现了 `Deref` trait 并从 `deref` 方法返回 `&Vec`。它也知道标准库实现了 `Vec`的 `Deref` trait,其 `deref` 方法返回 `&[T]`(我们也可以通过查阅 `Vec` 的 API 文档来发现这一点)。所以,在编译时,Rust 会发现它可以调用两次 `Deref::deref` 来将 `&Mp3` 变成 `&Vec` 再变成 `&[T]` 来满足 `compress_mp3` 的签名。这意味着我们可以少写一些代码!Rust 会多次分析 `Deref::deref` 的返回值类型直到它满足参数的类型,只要相关类型实现了 `Deref` trait。这些间接转换在编译时进行,所以利用解引用强制多态并没有运行时惩罚! +示例 15-15:如果 Rust 没有解引用强制多态则必须编写的代码 -类似于如何使用 `Deref` trait 重载 `&T` 的 `*` 运算符,`DerefMut` trait 用于重载 `&mut T` 的 `*` 运算符。 +`(*m)` 将 `MyBox` 解引用为 `String`。接着 `&` 和 `[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有解引用强制多态所有这些符号混在一起将更难以读写和理解。解引用强制多态使得 Rust 自动的帮我们处理这些转换。 + +当所涉及到的类型定义了 `Deref` trait,Rust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这写解析都发生在编译时,所以利用解引用强制多态并没有运行时惩罚! + +### 解引用强制多态如何与可变性交互 + + + + +类似于如何使用 `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`。对于可变引用也是一样。最后一个有些微妙:如果有一个可变引用,它也可以强转为一个不可变引用。反之则是 **不可能** 的:不可变引用永远也不能强转为可变引用。 +头两个情况除了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以直接得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。 -`Deref` trait 对于智能指针模式十分重要的原因在于智能指针可以被看作普通引用并被用于期望使用普通引用的地方。例如,无需重新定义方法和函数来直接获取智能指针。 \ No newline at end of file +最后一个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。 + + +