|
|
|
|
## 通过 `Deref` trait 将智能指针当作常规引用处理
|
|
|
|
|
|
|
|
|
|
> [ch15-02-deref.md](https://github.com/rust-lang/book/blob/main/src/ch15-02-deref.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 0514b1cf34c2eaab8285f43305c10a87f4ce34a0
|
|
|
|
|
|
|
|
|
|
实现 `Deref` trait 允许我们重载 **解引用运算符**(_dereference operator_)`*`(不要与乘法运算符或通配符相混淆)。通过这种方式实现 `Deref` trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。
|
|
|
|
|
|
|
|
|
|
让我们首先看看解引用运算符如何处理常规引用,接着尝试定义我们自己的类似 `Box<T>` 的类型并看看为何解引用运算符不能像引用一样工作。我们会探索如何实现 `Deref` trait 使得智能指针以类似引用的方式工作变为可能。最后,我们会讨论 Rust 的 **Deref 强制转换**(_deref coercions_)功能以及它是如何处理引用或智能指针的。
|
|
|
|
|
|
|
|
|
|
> 我们将要构建的 `MyBox<T>` 类型与真正的 `Box<T>` 有一个很大的区别:我们的版本不会在堆上储存数据。这个例子重点关注 `Deref`,所以其数据实际存放在何处,相比其类似指针的行为来说不算重要。
|
|
|
|
|
|
|
|
|
|
### 追踪指针的值
|
|
|
|
|
|
|
|
|
|
常规引用是一个指针类型,一种理解指针的方式是将其看成指向储存在其他某处值的箭头。在示例 15-6 中,创建了一个 `i32` 值的引用,接着使用解引用运算符来跟踪所引用的值:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-06/src/main.rs}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-6:使用解引用运算符来跟踪 `i32` 值的引用</span>
|
|
|
|
|
|
|
|
|
|
变量 `x` 存放了一个 `i32` 值 `5`。`y` 等于 `x` 的一个引用。可以断言 `x` 等于 `5`。然而,如果希望对 `y` 的值做出断言,必须使用 `*y` 来追踪引用所指向的值(也就是 **解引用**),这样编译器就可以比较实际的值了。一旦解引用了 `y`,就可以访问 `y` 所指向的整型值并可以与 `5` 做比较。
|
|
|
|
|
|
|
|
|
|
相反如果尝试编写 `assert_eq!(5, y);`,则会得到如下编译错误:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
{{#include ../listings/ch15-smart-pointers/output-only-01-comparing-to-reference/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
不允许比较数字的引用与数字,因为它们是不同的类型。必须使用解引用运算符追踪引用所指向的值。
|
|
|
|
|
|
|
|
|
|
### 像引用一样使用 `Box<T>`
|
|
|
|
|
|
|
|
|
|
可以使用 `Box<T>` 代替引用来重写示例 15-6 中的代码,示例 15-7 中 `Box<T>` 上使用的解引用运算符与示例 15-6 中引用上使用的解引用运算符有着一样的功能:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-07/src/main.rs}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-7:在 `Box<i32>` 上使用解引用运算符</span>
|
|
|
|
|
|
|
|
|
|
示例 15-7 相比示例 15-6 主要不同的地方就是将 `y` 设置为一个指向 `x` 值拷贝的 `Box<T>` 实例,而不是指向 `x` 值的引用。在最后的断言中,可以使用解引用运算符以 `y` 为引用时相同的方式追踪 `Box<T>` 的指针。接下来让我们通过实现自己的类型来探索 `Box<T>` 能这么做有何特殊之处。
|
|
|
|
|
|
|
|
|
|
### 自定义智能指针
|
|
|
|
|
|
|
|
|
|
为了体会默认情况下智能指针与引用的不同,让我们创建一个类似于标准库提供的 `Box<T>` 类型的智能指针。接着学习如何增加使用解引用运算符的功能。
|
|
|
|
|
|
|
|
|
|
从根本上说,`Box<T>` 被定义为包含一个元素的元组结构体,所以示例 15-8 以相同的方式定义了 `MyBox<T>` 类型。我们还定义了 `new` 函数来对应定义于 `Box<T>` 的 `new` 函数:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-08/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-8:定义 `MyBox<T>` 类型</span>
|
|
|
|
|
|
|
|
|
|
这里定义了一个结构体 `MyBox` 并声明了一个泛型参数 `T`,因为我们希望其可以存放任何类型的值。`MyBox` 是一个包含 `T` 类型元素的元组结构体。`MyBox::new` 函数获取一个 `T` 类型的参数并返回一个存放传入值的 `MyBox` 实例。
|
|
|
|
|
|
|
|
|
|
尝试将示例 15-7 中的代码加入示例 15-8 中并修改 `main` 使用我们定义的 `MyBox<T>` 类型代替 `Box<T>`。示例 15-9 中的代码不能编译,因为 Rust 不知道如何解引用 `MyBox`:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust,ignore,does_not_compile
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-09/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-9:尝试以使用引用和 `Box<T>` 相同的方式使用 `MyBox<T>`</span>
|
|
|
|
|
|
|
|
|
|
得到的编译错误是:
|
|
|
|
|
|
|
|
|
|
```console
|
|
|
|
|
{{#include ../listings/ch15-smart-pointers/listing-15-09/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`MyBox<T>` 类型不能解引用,因为我们尚未在该类型实现这个功能。为了启用 `*` 运算符的解引用功能,需要实现 `Deref` trait。
|
|
|
|
|
|
|
|
|
|
### 通过实现 `Deref` trait 将某类型像引用一样处理
|
|
|
|
|
|
|
|
|
|
如第十章 [“为类型实现 trait”][impl-trait] 部分所讨论的,为了实现 trait,需要提供 trait 所需的方法实现。`Deref` trait,由标准库提供,要求实现名为 `deref` 的方法,其借用 `self` 并返回一个内部数据的引用。示例 15-10 包含定义于 `MyBox` 之上的 `Deref` 实现:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-10/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-10:`MyBox<T>` 上的 `Deref` 实现</span>
|
|
|
|
|
|
|
|
|
|
`type Target = T;` 语法定义了用于此 trait 的关联类型。关联类型是一个稍有不同的定义泛型参数的方式,现在还无需过多的担心它;第十九章会详细介绍。
|
|
|
|
|
|
|
|
|
|
`deref` 方法体中写入了 `&self.0`,这样 `deref` 返回了我希望通过 `*` 运算符访问的值的引用。回忆一下第五章 [“使用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分 `.0` 用来访问元组结构体的第一个元素。示例 15-9 中的 `main` 函数中对 `MyBox<T>` 值的 `*` 调用现在可以编译并能通过断言了!
|
|
|
|
|
|
|
|
|
|
没有 `Deref` trait 的话,编译器只会解引用 `&` 引用类型。`deref` 方法向编译器提供了获取任何实现了 `Deref` trait 的类型的值,并且调用这个类型的 `deref` 方法来获取一个它知道如何解引用的 `&` 引用的能力。
|
|
|
|
|
|
|
|
|
|
当我们在示例 15-9 中输入 `*y` 时,Rust 事实上在底层运行了如下代码:
|
|
|
|
|
|
|
|
|
|
```rust,ignore
|
|
|
|
|
*(y.deref())
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Rust 将 `*` 运算符替换为先调用 `deref` 方法再进行普通解引用的操作,如此我们便不用担心是否还需手动调用 `deref` 方法了。Rust 的这个特性可以让我们写出行为一致的代码,无论是面对的是常规引用还是实现了 `Deref` 的类型。
|
|
|
|
|
|
|
|
|
|
`deref` 方法返回值的引用,以及 `*(y.deref())` 括号外边的普通解引用仍为必须的原因在于所有权。如果 `deref` 方法直接返回值而不是值的引用,其值(的所有权)将被移出 `self`。在这里以及大部分使用解引用运算符的情况下我们并不希望获取 `MyBox<T>` 内部值的所有权。
|
|
|
|
|
|
|
|
|
|
注意,每次当我们在代码中使用 `*` 时, `*` 运算符都被替换成了先调用 `deref` 方法再接着使用 `*` 解引用的操作,且只会发生一次,不会对 `*` 操作符无限递归替换,解引用出上面 `i32` 类型的值就停止了,这个值与示例 15-9 中 `assert_eq!` 的 `5` 相匹配。
|
|
|
|
|
|
|
|
|
|
### 函数和方法的隐式 Deref 强制转换
|
|
|
|
|
|
|
|
|
|
**Deref 强制转换**(_deref coercions_)将实现了 `Deref` trait 的类型的引用转换为另一种类型的引用。例如,Deref 强制转换可以将 `&String` 转换为 `&str`,因为 `String` 实现了 `Deref` trait 因此可以返回 `&str`。Deref 强制转换是 Rust 在函数或方法传参上的一种便利操作,并且只能作用于实现了 `Deref` trait 的类型。当这种特定类型的引用作为实参传递给和形参类型不同的函数或方法时将自动进行。这时会有一系列的 `deref` 方法被调用,把我们提供的类型转换成了参数所需的类型。
|
|
|
|
|
|
|
|
|
|
Deref 强制转换的加入使得 Rust 程序员编写函数和方法调用时无需增加过多显式使用 `&` 和 `*` 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。
|
|
|
|
|
|
|
|
|
|
作为展示 Deref 强制转换的实例,让我们使用示例 15-8 中定义的 `MyBox<T>`,以及示例 15-10 中增加的 `Deref` 实现。示例 15-11 展示了一个有着字符串 slice 参数的函数定义:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-11/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-11:`hello` 函数有着 `&str` 类型的参数 `name`</span>
|
|
|
|
|
|
|
|
|
|
可以使用字符串 slice 作为参数调用 `hello` 函数,比如 `hello("Rust");`。Deref 强制转换使得用 `MyBox<String>` 类型值的引用调用 `hello` 成为可能,如示例 15-12 所示:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-12/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-12:因为 Deref 强制转换,使用 `MyBox<String>` 的引用调用 `hello` 是可行的</span>
|
|
|
|
|
|
|
|
|
|
这里使用 `&m` 调用 `hello` 函数,其为 `MyBox<String>` 值的引用。因为示例 15-10 中在 `MyBox<T>` 上实现了 `Deref` trait,Rust 可以通过 `deref` 调用将 `&MyBox<String>` 变为 `&String`。标准库中提供了 `String` 上的 `Deref` 实现,其会返回字符串 slice,这可以在 `Deref` 的 API 文档中看到。Rust 再次调用 `deref` 将 `&String` 变为 `&str`,这就符合 `hello` 函数的定义了。
|
|
|
|
|
|
|
|
|
|
如果 Rust 没有实现 Deref 强制转换,为了使用 `&MyBox<String>` 类型的值调用 `hello`,则不得不编写示例 15-13 中的代码来代替示例 15-12:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-13/src/main.rs:here}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 15-13:如果 Rust 没有 Deref 强制转换则必须编写的代码</span>
|
|
|
|
|
|
|
|
|
|
`(*m)` 将 `MyBox<String>` 解引用为 `String`。接着 `&` 和 `[..]` 获取了整个 `String` 的字符串 slice 来匹配 `hello` 的签名。没有 Deref 强制转换所有这些符号混在一起将更难以读写和理解。Deref 强制转换使得 Rust 自动的帮我们处理这些转换。
|
|
|
|
|
|
|
|
|
|
当所涉及到的类型定义了 `Deref` trait,Rust 会分析这些类型并使用任意多次 `Deref::deref` 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用 Deref 强制转换并没有运行时损耗!
|
|
|
|
|
|
|
|
|
|
### Deref 强制转换如何与可变性交互
|
|
|
|
|
|
|
|
|
|
类似于如何使用 `Deref` trait 重载不可变引用的 `*` 运算符,Rust 提供了 `DerefMut` trait 用于重载可变引用的 `*` 运算符。
|
|
|
|
|
|
|
|
|
|
Rust 在发现类型和 trait 实现满足三种情况时会进行 Deref 强制转换:
|
|
|
|
|
|
|
|
|
|
* 当 `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`。第二种情况表明对于可变引用也有着相同的行为。
|
|
|
|
|
|
|
|
|
|
第三个情况有些微妙:Rust 也会将可变引用强转为不可变引用。但是反之是 **不可能** 的:不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要初始的不可变引用是数据唯一的不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。
|
|
|
|
|
|
|
|
|
|
[impl-trait]: ch10-02-traits.html#implementing-a-trait-on-a-type
|
|
|
|
|
[tuple-structs]: ch05-01-defining-structs.html#使用没有命名字段的元组结构体来创建不同的类型
|