You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/src/ch15-02-deref.md

331 lines
16 KiB

7 years ago
## 通过 `Deref` trait 将智能指针当作常规引用处理
8 years ago
> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch15-02-deref.md)
> <br>
7 years ago
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
8 years ago
7 years ago
实现 `Deref` trait 允许我们重载 **解引用运算符***dereference operator*`*`(与乘法运算符或 glob 运算符相区别)。通过这种方式实现 `Deref` trait 可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
8 years ago
7 years ago
<!-- Why would we want to override the dereference operator? Can you lay that
out? -->
<!-- Attempted above. /Carol -->
<!-- I'd suggest introducing what you mean by "convenient" here, if we are
using it as the reason we want to use Deref -->
<!-- I've removed convenient from the first paragraph and foreshadowed in a
different way in the below paragraph /Carol -->
让我们首先看看 `*` 如何处理引用,接着尝试定义我们自己的类 `Box<T>` 类型并看看为何 `*` 不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **解引用强制多态***deref coercions*)功能和它是如何一同处理引用或智能指针的。
7 years ago
### 通过 `*` 追踪指针的值
7 years ago
<!-- I want to avoid too much cross referencing, I think it can be distracting,
make the reader feel they need to flip back but they don't really, here -->
<!-- Ok, guess we went too far then! I've been adding *more* cross referencing
so that the reader can go back if they've forgotten something we've already
covered. /Carol -->
<!--Oh! I see, de-reference, meaning we cut the tie between the data and the
reference? I've assumed so above, please correct if not! -->
<!-- I wouldn't describe it as "cutting the tie"; the tie is still there. It's
more like we're following an arrow (the pointer) to find the value. Let us know
if this explanation is still unclear. /Carol -->
常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-8 中,创建了一个 `i32` 值的引用接着使用解引用运算符来跟踪所引用的数据:
<!-- We'll start with an example of dereferencing and re-allocating references
to `i32` values: -->
<!-- Is this what this is an example of? -->
<!-- No, there isn't any re-allocation happening here; allocation is a term
that means asking for more space in order to hold data (as we covered in
chapter 4). What were you trying to convey with "re-allocating", exactly? Have
we addressed whatever was confusing here before? /Carol -->
<!-- We've reworked the following sections in this chapter heavily because the
`Mp3` example seemed to be confusing with the metadata that was involved.
Interested to see if this breakdown works better or not. /Carol -->
<span class="filename">文件名: src/main.rs</span>
8 years ago
```rust
7 years ago
fn main() {
let x = 5;
let y = &x;
8 years ago
7 years ago
assert_eq!(5, x);
assert_eq!(5, *y);
8 years ago
}
7 years ago
```
<span class="caption">示例 15-8使用解引用运算符来跟踪 `i32` 值的引用</span>
变量 `x` 存放了一个 `i32``5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**)。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。
8 years ago
7 years ago
相反如果尝试编写 `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}`
8 years ago
```
7 years ago
不允许比较数字的引用与数字,因为它们是不同的类型。必须使用 `*` 追踪引用所指向的值。
8 years ago
7 years ago
### 像引用一样使用 `Box<T>`
8 years ago
可以重写示例 15-8 中的代码,使用 `Box<T>` 来代替引用,解引用运算符也一样能工作,如示例 15-9 所示:
8 years ago
7 years ago
<span class="filename">文件名: src/main.rs</span>
8 years ago
```rust
7 years ago
fn main() {
let x = 5;
let y = Box::new(x);
8 years ago
7 years ago
assert_eq!(5, x);
assert_eq!(5, *y);
8 years ago
}
7 years ago
```
<span class="caption">示例 15-9`Box<i32>` 上使用解引用运算符</span>
相比示例 15-8 唯一修改的地方就是将 `y` 设置为一个指向 `x` 值的 box 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 box 的指针。让我们通过实现自己的 box 类型来探索 `Box<T>` 能这么做有何特殊之处。
### 自定义智能指针
8 years ago
为了体会默认智能指针的行为不同于引用,让我们创建一个类似于标准库提供的 `Box<T>` 类型的智能指针。接着会学习如何增加使用解引用运算符的功能。
8 years ago
7 years ago
从根本上说,`Box<T>` 被定义为包含一个元素的元组结构体,所以示例 15-10 以相同的方式定义了 `MyBox<T>` 类型。我们还定义了 `new` 函数来对应定义于 `Box<T>``new` 函数:
<span class="filename">文件名: src/main.rs</span>
```rust
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
8 years ago
}
}
7 years ago
```
<span class="caption">示例 15-10定义 `MyBox<T>` 类型</span>
这里定义了一个结构体 `MyBox` 并声明了一个泛型 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。
尝试将示例 15-9 中的代码加入示例 15-10 中并修改 `main` 使用我们定义的 `MyBox<T>` 类型代替 `Box<T>`。示例 15-11 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`
<span class="filename">文件名: src/main.rs</span>
8 years ago
7 years ago
```rust,ignore
8 years ago
fn main() {
7 years ago
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
```
<span class="caption">示例 15-11尝试以使用引用和 `Box<T>` 相同的方式使用 `MyBox<T>`</span>
得到的编译错误是:
```text
error: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
```
`MyBox<T>` 类型不能解引用我们并没有为其实现这个功能。为了启用 `*` 运算符的解引用功能,可以实现 `Deref` trait。
### 实现 `Deref` trait 定义如何像引用一样对待某类型
如第十章所讨论的,为了实现 trait需要提供 trait 所需的方法实现。`Deref` trait由标准库提供要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-12 包含定义于 `MyBox` 之上的 `Deref` 实现:
<span class="filename">文件名: src/main.rs</span>
```rust
use std::ops::Deref;
# struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
8 years ago
}
```
7 years ago
<span class="caption">示例 15-12`MyBox<T>` 上的 `Deref` 实现</span>
`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。
8 years ago
7 years ago
<!-- Is it possible to just use a method for declaring a generic parameter we
have seen before, so we can focus on the deref trait here? -->
<!-- No, this is how the `Deref` trait is defined in the standard library, so
this is what you have to specify in order to implement it. /Carol -->
8 years ago
7 years ago
`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。示例 15-11 中的 `main` 函数中对 `MyBox<T>` 值的 `*` 调用现在可以编译并能通过断言了!
8 years ago
没有 `Deref` trait 的话,编译器可以解引用的只有 `&` 引用类型;有了 `Deref` trait 之后,对任何实现 `Deref` trait 的类型,编译器都能(通过解引用的形式)从其获取一个值。只要调用这个类型的 `deref` 方法,编译器就可以得到一个 `&` 引用,再对 `&` 引用进行解引用对它来说就是熟悉的操作了。
7 years ago
当我们在示例 15-11 中输入 `*y`Rust 事实上在底层运行了如下代码:
8 years ago
```rust,ignore
7 years ago
*(y.deref())
8 years ago
```
7 years ago
<!-- why is that happening behind the scenes, rather than us just calling this
up front? -->
<!-- we've tried to clarify below /Carol -->
Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行直接引用的操作,如此我们便不用担心是不是还需要手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。
7 years ago
`deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox<T>` 内部值的所有权。
注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-11 中 `assert_eq!``5` 相匹配。
8 years ago
### 函数和方法的隐式解引用强制多态
7 years ago
<!--Below -- "A deref coercion happens when..." So this isn't something the
reader is making happen, but something that just happens behind the scene? If
not, can you change this to an active tone? -->
<!-- Yes, it is something that happens behind the scenes, which is why we
describe it as implicit. /Carol -->
8 years ago
**解引用强制多态***deref coercions*)是 Rust 表现在函数或方法传参上的一种便利。其将实现了 `Deref` 的类型的引用转换为原始类型通过 `Deref` 所能够转换的类型的引用。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时,解引用强制多态将自动发生。这时会有一系列的 `deref` 方法被调用,把我们提供的类型转换成了参数所需的类型。
8 years ago
7 years ago
解引用强制多态的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&``*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。
作为展示解引用强制多态的实例,让我们使用示例 15-10 中定义的 `MyBox<T>`,以及示例 15-12 中增加的 `Deref` 实现。示例 15-13 展示了一个有着字符串 slice 参数的函数定义:
<span class="filename">文件名: src/main.rs</span>
```rust
fn hello(name: &str) {
println!("Hello, {}!", name);
8 years ago
}
```
7 years ago
<span class="caption">示例 15-13`hello` 函数有着 `&str` 类型的参数 `name`</span>
8 years ago
可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。解引用强制多态使得用 `MyBox<String>` 类型值的引用调用 `hello` 成为可能,如示例 15-14 所示:
7 years ago
<span class="filename">文件名: src/main.rs</span>
```rust
# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
# fn new(x: T) -> MyBox<T> {
# MyBox(x)
# }
# }
#
# impl<T> Deref for MyBox<T> {
# 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);
}
8 years ago
```
7 years ago
<span class="caption">示例 15-14因为解引用强制多态使用 `MyBox<String>` 的引用调用 `hello` 是可行的</span>
8 years ago
7 years ago
这里使用 `&m` 调用 `hello` 函数,其为 `MyBox<String>` 值的引用。因为示例 15-12 中在 `MyBox<T>` 上实现了 `Deref` traitRust 可以通过 `deref` 调用将 `&MyBox<String>` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref``&String` 变为 `&str`,这就符合 `hello` 函数的定义了。
8 years ago
7 years ago
如果 Rust 没有实现解引用强制多态,为了使用 `&MyBox<String>` 类型的值调用 `hello`,则不得不编写示例 15-15 中的代码来代替示例 15-14
<span class="filename">文件名: src/main.rs</span>
```rust
# use std::ops::Deref;
#
# struct MyBox<T>(T);
#
# impl<T> MyBox<T> {
# fn new(x: T) -> MyBox<T> {
# MyBox(x)
# }
# }
#
# impl<T> Deref for MyBox<T> {
# 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)[..]);
}
8 years ago
```
7 years ago
<span class="caption">示例 15-15如果 Rust 没有解引用强制多态则必须编写的代码</span>
`(*m)``MyBox<String>` 解引用为 `String`。接着 `&``[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有解引用强制多态所有这些符号混在一起将更难以读写和理解。解引用强制多态使得 Rust 自动的帮我们处理这些转换。
8 years ago
当所涉及到的类型定义了 `Deref` traitRust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用解引用强制多态并没有运行时惩罚!
7 years ago
### 解引用强制多态如何与可变性交互
<!-- below: are we talking about any mutable references, or are we talking
about mutable generic types, below? Can you make sure it's clear throughout, I
wasn't 100% -->
<!-- I'm not sure what you're asking, *types* don't have the property of
mutability or immutability, it's the variables or references to *instances* of
those types that are mutable or immutable. Also the way to say "any mutable
reference" is with `&mut` and a generic type parameter. Is that what's
confusing? /Carol -->
类似于如何使用 `Deref` trait 重载不可变引用的 `*` 运算符Rust 提供了 `DerefMut` trait 用于重载可变引用的 `*` 运算符。
8 years ago
Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态:
7 years ago
<!-- Would it make sense to move this list to the start of the deref section?
-->
<!-- I don't think this list makes very much sense until you understand what
deref coercion *is*. Can you elaborate on why you think it should be moved to
the beginning? /Carol -->
7 years ago
*`T: Deref<Target=U>` 时从 `&T``&U`
*`T: DerefMut<Target=U>` 时从 `&mut T``&mut U`
*`T: Deref<Target=U>` 时从 `&mut T``&U`
8 years ago
7 years ago
头两个情况除了可变性之外是相同的:第一种情况表明如果有一个 `&T`,而 `T` 实现了返回 `U` 类型的 `Deref`,则可以直接得到 `&U`。第二种情况表明对于可变引用也有着相同的行为。
最后一个情况有些微妙Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的不可变引用永远也不能强转为可变引用。因为根据借用规则如果有一个可变引用其必须是这些数据的唯一引用否则程序将无法编译。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用而借用规则无法保证这一点。因此Rust 无法假设将不可变引用转换为可变引用是可能的。
8 years ago
7 years ago
<!-- Why does it coerce to an immutable reference, and why cant it go the other
way?-->
<!-- Elaborated above /Carol-->