mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2025-05-13 02:48:05 +08:00
Compare commits
2 Commits
9a7f193bde
...
8510cae4e9
Author | SHA1 | Date | |
---|---|---|---|
|
8510cae4e9 | ||
|
1615d8fe5b |
@ -159,7 +159,7 @@ Rust 的借用检查器无法理解我们要借用这个 slice 的两个不同
|
||||
|
||||
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,`extern`,有助于创建和使用 **外部函数接口**(*Foreign Function Interface*,FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
|
||||
|
||||
示例 20-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任:
|
||||
示例 20-8 展示了如何集成 C 标准库中的 `abs` 函数。`extern` 块中声明的函数在 Rust 代码中通常是不安全的因此 `extern` 块本身也必须标注 `unsafe`。之所以如此,是因为其他语言不会强制执行 Rust 的规则,Rust 也无法检查这些约束,因此程序员有责任确保调用的安全性。
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -169,36 +169,52 @@ Rust 的借用检查器无法理解我们要借用这个 slice 的两个不同
|
||||
|
||||
<span class="caption">示例 20-8: 声明并调用另一个语言中定义的 `extern` 函数</span>
|
||||
|
||||
在 `extern "C"` 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。`"C"` 部分定义了外部函数所使用的 **应用二进制接口**(*application binary interface*,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。`"C"` ABI 是最常见的,并遵循 C 编程语言的 ABI。
|
||||
在 `unsafe extern "C"` 块中,我们列出了希望能够调用的另一个语言中的外部函数的签名和名称。`"C"` 部分定义了外部函数所使用的 **应用二进制接口**(*application binary interface*,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。`"C"` ABI 是最常见的,并遵循 C 编程语言的 ABI。有关 Rust 支持的所有 ABI 的信息请参见 [the Rust Reference][ABI]。
|
||||
|
||||
> #### 从其它语言调用 Rust 函数
|
||||
>
|
||||
> 也可以使用 `extern` 来创建一个允许其他语言调用 Rust 函数的接口。不同于创建整个 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并为相关函数指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
|
||||
>
|
||||
> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问:
|
||||
>
|
||||
> ```rust
|
||||
> #[no_mangle]
|
||||
> pub extern "C" fn call_from_c() {
|
||||
> println!("Just called a Rust function from C!");
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> `extern` 的使用无需 `unsafe`。
|
||||
`unsafe extern` 中声明的任何项都隐式地是 `unsafe` 的。然而,一些 FFI 函数**可以**安全地调用。例如,C 标准库中的 `abs` 函数没有任何内存安全方面的考量并且我们知道它可以使用任何 `i32` 调用。在类似这样的例子中,我们可以使用 `safe` 关键字来表明这个特定的函数即便是在 `unsafe extern` 块中也是可以安全调用的。一旦我们做出这个修改,调用它不再需要 `unsafe` 块,如示例 20-9 所示。
|
||||
|
||||
### 访问或修改可变静态变量
|
||||
<figure class="listing">
|
||||
|
||||
目前为止全书都尽量避免讨论 **全局变量**(*global variables*),Rust 确实支持它们,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
|
||||
|
||||
全局变量在 Rust 中被称为 **静态**(*static*)变量。示例 20-9 展示了一个拥有字符串 slice 值的静态变量的声明和应用:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
<span class="file-name">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-09/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-9: 定义和使用一个不可变静态变量</span>
|
||||
<figcaption>示例 20-9:在 `unsafe extern` 块中显式地标记一个函数为 `safe` 并安全地调用它</figcaption>
|
||||
|
||||
</figure>
|
||||
|
||||
将一个函数标记为 `safe` 并不会固有地使其变得安全!相反,这像是一个对 Rust 的承诺表明它**是**安全的。确保履行这个承诺仍然是你的责任!
|
||||
|
||||
> #### 从其它语言调用 Rust 函数
|
||||
>
|
||||
> 也可以使用 `extern` 来创建一个允许其它语言调用 Rust 函数的接口。不同于创建整个 `extern` 块,就在 `fn` 关键字之前增加 `extern` 关键字并为相关函数指定所用到的 ABI。还需增加 `#[no_mangle]` 注解来告诉 Rust 编译器不要 mangle 此函数的名称。*Mangling* 指编译器将我们命名的函数名更改为包含更多供其他编译过程使用的信息的名称,不过可读性较差。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。这是不安全的因为在没有内置 mangling 的时候在库之间可能有命名冲突,所以确保所选的名称可以不用 mangling 地安全导出是我们的责任。
|
||||
>
|
||||
> 在如下的例子中,一旦其编译为动态库并从 C 语言中链接,`call_from_c` 函数就能够在 C 代码中访问:
|
||||
>
|
||||
> ```rust
|
||||
> #[unsafe(no_mangle)]
|
||||
> pub extern "C" fn call_from_c() {
|
||||
> println!("Just called a Rust function from C!");
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> 这种 `extern` 用法只在属性中需要 `unsafe`,而不需要在 `extern` 块本身使用 `unsafe`。
|
||||
|
||||
### 访问或修改可变静态变量
|
||||
|
||||
在本书中,我们尚未讨论过 **全局变量**(*global variables*),Rust 确实支持它们,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
|
||||
|
||||
全局变量在 Rust 中被称为 **静态**(*static*)变量。示例 20-9 展示了一个拥有字符串 slice 值的静态变量的声明和使用:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-10/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-10: 定义和使用一个不可变静态变量</span>
|
||||
|
||||
静态(`static`)变量类似于第三章 [“变量和常量的区别”][differences-between-variables-and-constants] 部分讨论的常量。通常静态变量的名称采用 `SCREAMING_SNAKE_CASE` 写法。静态变量只能储存拥有 `'static` 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
|
||||
|
||||
@ -206,40 +222,71 @@ Rust 的借用检查器无法理解我们要借用这个 slice 的两个不同
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-10/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-10: 读取或修改一个可变静态变量是不安全的</span>
|
||||
|
||||
就像常规变量一样,我们使用 `mut` 关键来指定可变性。任何读写 `COUNTER` 的代码都必须位于 `unsafe` 块中。这段代码可以编译并如期打印出 `COUNTER: 3`,因为这是单线程的。拥有多个线程访问 `COUNTER` 则可能导致数据竞争。
|
||||
|
||||
拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用第十六章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
|
||||
|
||||
### 实现不安全 trait
|
||||
|
||||
`unsafe` 的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 `trait` 之前增加 `unsafe` 关键字将 trait 声明为 `unsafe`,同时 trait 的实现也必须标记为 `unsafe`,如示例 20-11 所示:
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-11/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-11: 定义并实现不安全 trait</span>
|
||||
<span class="caption">示例 20-11: 读取或修改一个可变静态变量是不安全的</span>
|
||||
|
||||
通过 `unsafe impl`,我们承诺将保证编译器所不能验证的不变量。
|
||||
就像常规变量一样,我们使用 `mut` 关键字来指定可变性。任何读写 `COUNTER` 的代码都必须位于 `unsafe` 块中。这段代码可以编译并如期打印出 `COUNTER: 3`,因为这是单线程的。拥有多个线程访问 `COUNTER` 则可能导致数据竞争,所以这是未定义行为。因此,我们需要将整个函数标记为 `unsafe`,并在文档注释中说明其安全性限制,以便调用者明确哪些操作是安全的、哪些是不安全的。
|
||||
|
||||
作为一个例子,回忆第十六章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait,编译器会自动为完全由 `Send` 和 `Sync` 类型组成的类型自动实现它们。如果实现了一个包含一些不是 `Send` 或 `Sync` 的类型,比如裸指针,并希望将此类型标记为 `Send` 或 `Sync`,则必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。
|
||||
每当我们编写一个不安全函数,惯常做法是编写一个以 `SAFETY` 开头的注释并解释调用者需要做什么才可以安全地调用该方法。同理,当我们进行不安全操作时,惯常做法是编写一个以 `SAFETY` 开头并解释安全性规则是如何维护的。
|
||||
|
||||
另外,编译器不会允许你创建一个可变静态变量的引用。你只能通过用裸指针解引用操作符创建的裸指针访问它。这包括引用的创建时不可见的情况,例如这个代码示例中用于 `println!` 的情况。可变静态变量只能通过裸指针创建的要求有助于确保使用它们的安全要求更为明确。
|
||||
|
||||
拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。在任何可能的情况下,请优先使用第十六章讨论的并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
|
||||
|
||||
### 实现不安全 trait
|
||||
|
||||
我们可以使用 `unsafe` 来实现一个不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时该 trait 就是不安全的。可以在 `trait` 之前增加 `unsafe` 关键字将 trait 声明为 `unsafe`,同时 trait 的实现也必须标记为 `unsafe`,如示例 20-12 所示:
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-12/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-12: 定义并实现不安全 trait</span>
|
||||
|
||||
通过 `unsafe impl`,我们承诺将保证编译器所不能验证的不变式。
|
||||
|
||||
作为一个例子,回忆第十六章 [“使用 `Sync` 和 `Send` trait 的可扩展并发”][extensible-concurrency-with-the-sync-and-send-traits] 部分中的 `Sync` 和 `Send` 标记 trait:如果我们的类型完全由实现了 `Send` 与 `Sync` 的其他类型组成,编译器会自动为其实现这些 trait。如果我们定义的类型包含某些未实现 `Send` 或 `Sync` 的类型,例如裸指针,但又想将该类型标记为 `Send` 或 `Sync`,就必须使用 `unsafe`。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 `unsafe` 表明。
|
||||
|
||||
### 访问联合体中的字段
|
||||
|
||||
仅适用于 `unsafe` 的最后一个操作是访问 **联合体** 中的字段,`union` 和 `struct` 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看 [参考 Rust 文档][reference] 了解有关联合体的更多信息。
|
||||
最后一个只能在 `unsafe` 块中执行的操作是访问(union)中的字段。`union` 和 `struct` 类似,但是在一个实例中同时只能使用一个已声明的字段。联合体主要用于和 C 代码中的联合体进行交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看 [the Rust Reference][unions] 了解有关联合体的更多信息。
|
||||
|
||||
### 使用 miri 检查不安全代码
|
||||
|
||||
当编写不安全代码时,你可能会想要检查编写的代码是否真的安全正确。最好的方式之一是使用 Miri,一个用来检测未定义行为的 Rust 官方工具。鉴于借用检查器是一个在编译时工作的**静态**工具,Miri 是一个在运行时工作的**动态**工具。它通过运行程序,或者测试集来检查代码,并检测你是否违反了它理解的 Rust 应该如何工作的规则。
|
||||
|
||||
使用 Miri 要求使用 nightly 版本的 Rust(我们在[附录 G:Rust 是如何开发的与 “Nightly Rust”][nightly]中有更多讨论)。你可以通过输入 `rustu +nightly component add miri` 来同时安装 nightly 版本的 Rust 和 Miri。这并不会改变你项目正在使用的 Rust 版本;它只是为你的系统增加了这个工具所以你可以在需要的时候使用它。你可以通过输入 `cargo +nightly miri run` or `cargo +nightly miri test` 在项目中使用 Miri。
|
||||
|
||||
作为一个它是如何有用的例子,考虑一下对示例 20-11 运行它时会发生什么。
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-11/output.txt}}
|
||||
```
|
||||
|
||||
Miri 正确地警告了我们共享了可变数据的引用。这里,Miri 只是发出了一个警告因为在这个例子中并不能保证是未定义行为,它也没有告诉我们如何修复问题。但是至少我们知道这里有未定义行为的风险并接着可以思考如何使代码变得安全。在一些例子中,Miri 也可以检测真正的错误 -- **确定**是错误的代码模式 -- 并提出如何修复这些错误的推荐方案。
|
||||
|
||||
Miri 并不能捕获编写不安全代码时可能出现的所有错误。Miri 是一个动态分析工具,因此它只能捕获代码实际运行时出现的问题。这意味着需要将其与良好的测试技术相结合以增强你对所编写的不安全代码的信心。Miri 也不能覆盖代码所有的不可靠的地方。
|
||||
|
||||
换句话说:如果 Miri **可以**捕获一个问题,你知道这里有个 bug,不过仅仅是因为 miri **没有**捕获一个 bug 并不意味着这里没有问题。但是它可以捕获很多问题。尝试对本章中的其它不安全代码示例运行它来看看它会说些什么!
|
||||
|
||||
你可以在 [Miri 的 GitHub 仓库][miri]了解更多信息。
|
||||
|
||||
### 何时使用不安全代码
|
||||
|
||||
使用 `unsafe` 来进行这五个操作(超能力)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注可以更容易地在错误发生时追踪问题的源头。
|
||||
使用 `unsafe` 来进行这五个操作(超能力)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 `unsafe` 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 `unsafe` 代码时,是可以这么做的,通过使用显式的 `unsafe` 标注可以更容易地在错误发生时追踪问题的源头。每当编写不安全代码时,都可以借助 Miri 来更加自信地验证所写代码是否遵循 Rust 的规则。
|
||||
|
||||
若想更深入地了解如何高效使用不安全 Rust,请阅读 Rust 关于该主题的官方指南 [Rustonomicon][nomicon]。
|
||||
|
||||
[dangling-references]: ch04-02-references-and-borrowing.html#悬垂引用dangling-references
|
||||
[ABI]: https://doc.rust-lang.org/reference/items/external-blocks.html#abi
|
||||
[differences-between-variables-and-constants]: ch03-01-variables-and-mutability.html#常量
|
||||
[extensible-concurrency-with-the-sync-and-send-traits]: ch16-04-extensible-concurrency-sync-and-send.html#使用-sync-和-send-trait-的可扩展并发
|
||||
[the-slice-type]: ch04-03-slices.html#slice-类型
|
||||
[reference]: https://doc.rust-lang.org/reference/items/unions.html
|
||||
[unions]: https://doc.rust-lang.org/reference/items/unions.html
|
||||
[miri]: https://github.com/rust-lang/miri
|
||||
[editions]: appendix-05-editions.html
|
||||
[nightly]: appendix-07-nightly-rust.html
|
||||
[nomicon]: https://doc.rust-lang.org/nomicon/
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
> [ch20-02-advanced-traits.md](https://github.com/rust-lang/book/blob/main/src/ch20-02-advanced-traits.md)
|
||||
> <br>
|
||||
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
|
||||
> commit 56ec353290429e6547109e88afea4de027b0f1a9
|
||||
|
||||
第十章 [“trait:定义共同行为”][traits-defining-shared-behavior] 部分,我们第一次涉及到了 trait,不过我们并没有覆盖一些较为高级的细节。现在我们更加了解 Rust 了,可以深入理解其本质了。
|
||||
在第十章[“trait:定义共同行为”][traits-defining-shared-behavior]部分,我们第一次涉及到了 trait,不过我们并没有覆盖一些较为高级的细节。现在你对 Rust 已经有了更多了解,我们可以深入探究了。
|
||||
|
||||
### 关联类型在 trait 定义中指定占位符类型
|
||||
### 关联类型
|
||||
|
||||
**关联类型**(*associated types*)让我们可以在 trait 里面增加一个待定义的类型(类型占位符),将类型占位符与 trait 相关联,这样 trait 的方法签名中就可以使用这些占位符类型。trait 的实现者在实现这个 trait 的时候,会指定一个具体类型,来替换掉这个占位符。这样,我们可以在一个 trait 中通过占位符使用不同类型,在实现此 trait 时才需要指定这些类型具体是什么。
|
||||
**关联类型**(*associated types*)将一个类型占位符与 trait 相关联,使得该 trait 的方法定义可以在签名中使用这些占位符类型。该 trait 的实现者会为每个具体实现指定要使用的具体类型来替代占位符类型。这样,我们就能在定义 trait 时使用占位符类型,而无需预先知道这些类型的具体内容,直到实现该 trait 时再进行指定。
|
||||
|
||||
我们之前提到,本章所描述的大部分内容都较少使用。关联类型则比较适中;它们比本书其他的内容要少见,不过比本章中的很多内容要更常见。
|
||||
我们之前提到,本章所讨论的大多数高级特性都很少需要。关联类型则比较适中:它们的使用频率低于本书其他部分讲解的特性,但又高于本章中许多其他特性。
|
||||
|
||||
一个带有关联类型的 trait 的例子是标准库提供的 `Iterator` trait。它有一个叫做 `Item` 的关联类型来替代遍历的值的类型。`Iterator` trait 的定义如示例 20-13 所示:
|
||||
|
||||
@ -20,10 +20,9 @@
|
||||
|
||||
<span class="caption">示例 20-13: `Iterator` trait 的定义中带有关联类型 `Item`</span>
|
||||
|
||||
`Item` 是一个占位符类型,同时 `next` 方法的定义表明它返回 `Option<Self::Item>` 类型的值。这个 trait 的实现者会指定 `Item` 的具体类型,无论实现者指定何种类型,`next` 方法都会返回一个包含了此具体类型值的 `Option`。
|
||||
`Item` 是一个占位符类型,同时 `next` 方法的定义表明它返回 `Option<Self::Item>` 类型的值。`Iterator` trait 的实现者会指定 `Item` 的具体类型,于是 `next` 方法就会返回一个包含该具体类型值的 `Option`。
|
||||
|
||||
关联类型看起来有点像泛型:后者允许定义一个函数时,暂不指定其可以处理的类型。为了体现这两者的区别,请看下面的例子。
|
||||
这个例子为 `Counter` 结构体实现了 `Iterator` trait,其中指定 `Item` 的类型为 `u32`:
|
||||
关联类型可能看起来与泛型类似,后者允许我们在定义函数时不必指定它可以处理的类型。为了体现这两者的区别,我们来看一个名为 `Counter` 的类型上的 `Iterator` trait 实现,其中指定 `Item` 的类型为 `u32`:
|
||||
|
||||
<span class="filename">文件名:src/lib.rs</span>
|
||||
|
||||
@ -31,27 +30,27 @@
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-22-iterator-on-counter/src/lib.rs:ch19}}
|
||||
```
|
||||
|
||||
这个语法类似于泛型。那么为什么 `Iterator` trait 不像下面示例 20-13 那样,使用泛型来定义呢?
|
||||
这种语法看起来与泛型类似。那么为什么不直接像示例 20-14 那样,用泛型来定义 `Iterator` trait 呢?
|
||||
|
||||
```rust,noplayground
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-13/src/lib.rs}}
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-14/src/lib.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-13: 一个使用泛型的 `Iterator` trait 假想定义</span>
|
||||
<span class="caption">示例 20-14: 一个使用泛型的 `Iterator` trait 假想定义</span>
|
||||
|
||||
区别在于当如示例 20-13 那样使用泛型时,则不得不在每一个实现中标注类型。这是因为我们也可以实现为 `Iterator<String> for Counter`,或任何其他类型,这样就可以有多个 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 `Counter` 的 `next` 方法时,必须提供类型注解来表明希望使用 `Iterator` 的哪一个实现。
|
||||
区别在于当如示例 20-14 那样使用泛型时,则不得不在每一个实现中标注类型;这是因为我们也可以实现为 `Iterator<String> for Counter`,或任何其他类型,这样就可以有多个针对 `Counter` 的 `Iterator` 的实现。换句话说,当 trait 有泛型参数时,可以多次实现这个 trait,每次都使用不同的具体泛型参数类型。当我们在 `Counter` 上调用 `next` 方法时,就必须通过类型注解来指明要使用哪一个 `Iterator` 的实现。
|
||||
|
||||
有了关联类型,在实现时就无需标注类型,因为不能多次实现这个 trait。对于示例 20-12 使用关联类型的定义,我们只能选择一次 `Item` 会是什么类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。
|
||||
使用关联类型后,则无需标注类型,因为不能对同一个类型多次实现该 trait。在示例 20-13 中使用关联类型的定义里,我们只能为 `Item` 选择一次具体类型,因为只能有一个 `impl Iterator for Counter`。当调用 `Counter` 的 `next` 时不必每次指定我们需要 `u32` 值的迭代器。
|
||||
|
||||
关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符。关联类型通常以它的用途来命名,并且我们最好在 API 文档中为关联类型编写文档。
|
||||
关联类型也会成为 trait 契约的一部分:trait 的实现必须提供一个类型来替代关联类型占位符。关联类型通常以它的用途来命名,在 API 文档中对关联类型进行说明也是一种良好实践。
|
||||
|
||||
### 默认泛型类型参数和运算符重载
|
||||
|
||||
当使用泛型类型参数时,可以为泛型指定一个默认的具体类型。如果默认类型就足够的话,这消除了为具体类型实现 trait 的需要。为泛型类型指定默认类型的语法是在声明泛型类型时使用 `<PlaceholderType=ConcreteType>`。
|
||||
|
||||
这种情况的一个非常好的例子是使用 **运算符重载**(*Operator overloading*),这是指在特定情况下自定义运算符(比如 `+`)行为的操作。
|
||||
这种技术的一个很好的示例是 **运算符重载** (*operator overloading*),即在特定情况下自定义运算符(比如 `+`)行为的操作。
|
||||
|
||||
Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std::ops` 中所列出的运算符和相应的 trait 可以通过实现运算符相关 trait 来重载。例如,示例 20-14 中展示了如何在 `Point` 结构体上实现 `Add` trait 来重载 `+` 运算符,这样就可以将两个 `Point` 实例相加了:
|
||||
Rust 并不允许创建自定义运算符或重载任意运算符,但可以通过实现 `std::ops` 中列出的运算符相关 trait 来重载它们。例如,在示例 20-15 中我们重载 `+` 运算符来将两个 `Point` 实例相加。我们通过在 `Point` 结构体上实现 `Add` trait 来实现这一点。
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -59,7 +58,7 @@ Rust 并不允许创建自定义运算符或重载任意运算符,不过 `std:
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-15/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-14: 实现 `Add` trait 重载 `Point` 实例的 `+` 运算符</span>
|
||||
<span class="caption">示例 20-15: 实现 `Add` trait 重载 `Point` 实例的 `+` 运算符</span>
|
||||
|
||||
`add` 方法将两个 `Point` 实例的 `x` 值和 `y` 值分别相加来创建一个新的 `Point`。`Add` trait 有一个叫做 `Output` 的关联类型,它用来决定 `add` 方法的返回值类型。
|
||||
|
||||
@ -73,19 +72,19 @@ trait Add<Rhs=Self> {
|
||||
}
|
||||
```
|
||||
|
||||
这些代码看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 `Rhs=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`Rhs` 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `Rhs` 的具体类型,`Rhs` 的类型将是默认的 `Self` 类型,也就是在其上实现 `Add` 的类型。
|
||||
这些代码看来应该很熟悉:一个带有一个方法和一个关联类型的 trait。新增的部分是 `Rhs=Self`:这个语法叫做 **默认类型参数**(*default type parameters*)。`Rhs` 是一个泛型类型参数(“right-hand side” 的缩写),它用于定义 `add` 方法中的 `rhs` 参数。如果实现 `Add` trait 时不指定 `Rhs` 的具体类型,`Rhs` 的类型将默认为 `Self`,即正在实现 `Add` 的类型。
|
||||
|
||||
当为 `Point` 实现 `Add` 时,使用了默认的 `Rhs`,因为我们希望将两个 `Point` 实例相加。让我们看看一个实现 `Add` trait 时希望自定义 `Rhs` 类型而不是使用默认类型的例子。
|
||||
|
||||
这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。(这种将现有类型简单封装进另一个结构体的方式被称为 **newtype 模式**(*newtype pattern*,之后的 [“为了类型安全和抽象而使用 newtype 模式”][newtype] 部分会详细介绍。)我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为 `Rhs`,如示例 20-15 所示。
|
||||
这里有两个存放不同单元值的结构体,`Millimeters` 和 `Meters`。这种将现有类型简单封装进另一个结构体的方式被称为 **newtype 模式**(*newtype pattern*),之后的[“使用 newtype 模式在外部类型上实现外部 trait”][newtype]部分会做详细介绍。我们希望能够将毫米值与米值相加,并让 `Add` 的实现正确处理单位转换。可以为 `Millimeters` 实现 `Add` 并以 `Meters` 作为 `Rhs`,如示例 20-16 所示。
|
||||
|
||||
<span class="filename">文件名:src/lib.rs</span>
|
||||
|
||||
```rust,noplayground
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-15/src/lib.rs}}
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-16/src/lib.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-15: 在 `Millimeters` 上实现 `Add`,以便能够将 `Millimeters` 与 `Meters` 相加</span>
|
||||
<span class="caption">示例 20-16: 在 `Millimeters` 上实现 `Add`,以便能够将 `Millimeters` 与 `Meters` 相加</span>
|
||||
|
||||
为了使 `Millimeters` 和 `Meters` 能够相加,我们指定 `impl Add<Meters>` 来设定 `Rhs` 类型参数的值而不是使用默认的 `Self`。
|
||||
|
||||
@ -94,25 +93,15 @@ trait Add<Rhs=Self> {
|
||||
* 扩展类型而不破坏现有代码。
|
||||
* 在大部分用户都不需要的特定情况进行自定义。
|
||||
|
||||
标准库的 `Add` trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 `Add` trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
|
||||
标准库的 `Add` trait 就是第二个目的的一个例子:大部分时候你会将两个相似的类型相加,但 `Add` trait 也提供了自定义额外行为的能力。在 `Add` trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
|
||||
|
||||
第一个目的是相似的,但过程是反过来的:如果需要为现有 trait 增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。
|
||||
第一个目的与第二个相似但方向相反:如果需要为现有 trait 增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。
|
||||
|
||||
### 完全限定语法与消歧义:调用相同名称的方法
|
||||
### 在同名方法之间消歧义
|
||||
|
||||
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。甚至直接在类型上实现开始已经有的同名方法也是可能的!
|
||||
Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法,也不能阻止为同一类型同时实现这两个 trait。同时也可以直接在类型上实现一个与 trait 方法同名的方法。
|
||||
|
||||
不过,当调用这些同名方法时,需要告诉 Rust 我们希望使用哪一个。考虑一下示例 20-16 中的代码,这里定义了 trait `Pilot` 和 `Wizard` 都拥有方法 `fly`。接着在一个本身已经实现了名为 `fly` 方法的类型 `Human` 上实现这两个 trait。每一个 `fly` 方法都进行了不同的操作:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-16/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-16: 两个 trait 定义为拥有 `fly` 方法,并在直接定义有 `fly` 方法的 `Human` 类型上实现这两个 trait</span>
|
||||
|
||||
当调用 `Human` 实例的 `fly` 时,编译器默认调用直接实现在类型上的方法,如示例 20-17 所示。
|
||||
当调用这些同名方法时,需要告诉 Rust 我们想要使用哪一个。考虑一下示例 20-17 中的代码,这里我们定义了两个 trait,`Pilot` 和 `Wizard`,它们都拥有名为 `fly` 的方法。接着在一个本身已经实现了名为 `fly` 方法的类型 `Human` 上实现这两个 trait。每一个 `fly` 方法都进行了不同的操作:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -120,11 +109,9 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-17/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-17: 调用 `Human` 实例的 `fly`</span>
|
||||
<span class="caption">示例 20-17: 两个 trait 定义为拥有 `fly` 方法,并在直接定义有 `fly` 方法的 `Human` 类型上实现这两个 trait</span>
|
||||
|
||||
运行这段代码会打印出 `*waving arms furiously*`,这表明 Rust 调用了直接实现在 `Human` 上的 `fly` 方法。
|
||||
|
||||
为了能够调用 `Pilot` trait 或 `Wizard` trait 的 `fly` 方法,我们需要使用更明显的语法以便能指定我们指的是哪个 `fly` 方法。这个语法展示在示例 20-18 中:
|
||||
当调用 `Human` 实例的 `fly` 时,编译器默认调用直接实现在该类型上的方法,如示例 20-18 所示。
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -132,81 +119,93 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-18/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-18: 指定我们希望调用哪一个 trait 的 `fly` 方法</span>
|
||||
<span class="caption">示例 20-18: 调用一个 `Human` 实例的 `fly`</span>
|
||||
|
||||
在方法名前指定 trait 名向 Rust 澄清了我们希望调用哪个 `fly` 实现。也可以选择写成 `Human::fly(&person)`,这等同于示例 20-18 中的 `person.fly()`,不过如果无需消歧义的话这么写就有点长了。
|
||||
运行这段代码会打印出 `*waving arms furiously*`,这表明 Rust 调用了直接实现在 `Human` 上的 `fly` 方法。
|
||||
|
||||
运行这段代码会打印出:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-18/output.txt}}
|
||||
```
|
||||
|
||||
因为 `fly` 方法获取一个 `self` 参数,如果有两个 **类型** 都实现了同一 **trait**,Rust 可以根据 `self` 的类型计算出应该使用哪一个 trait 实现。
|
||||
|
||||
然而,并非所有关联函数都有 `self` 参数,比如非方法(non-method)函数。当存在多个类型或者 trait 定义了相同函数名的非方法函数时,Rust 就不总是能计算出我们期望的是哪一个类型,除非使用 **完全限定语法**(*fully qualified syntax*)。例如示例 20-19 中的创建了一个希望将所有小狗叫做 *Spot* 的动物收容所的 trait。`Animal` trait 有一个关联非方法函数 `baby_name`。结构体 `Dog` 实现了 `Animal`,同时又直接提供了关联非方法函数 `baby_name`。
|
||||
为了能够调用 `Pilot` trait 或 `Wizard` trait 的 `fly` 方法,需要使用更明确的语法来指定具体要调用的 `fly` 方法。示例 20-19 演示了这种语法。
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-19/src/main.rs}}
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-19/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-19: 一个带有关联函数的 trait 和一个带有同名关联函数并实现了此 trait 的类型</span>
|
||||
<span class="caption">示例 20-19: 指定我们希望调用哪一个 trait 的 `fly` 方法</span>
|
||||
|
||||
`Dog` 上定义的关联函数 `baby_name` 的实现代码将所有的小狗起名为 Spot。`Dog` 类型还实现了 `Animal` trait,它描述了所有动物的共有的特征。小狗被称为 puppy,这表现为 `Dog` 的 `Animal` trait 实现中与 `Animal` trait 相关联的函数 `baby_name`。
|
||||
在方法名前指定 trait 名称可让 Rust 明确我们想调用哪个 `fly` 实现。也可以选择写成 `Human::fly(&person)`,这等同于示例 20-19 中的 `person.fly()`,不过如果无需消歧义的话这么写就有点冗长了。
|
||||
|
||||
在 `main` 调用了 `Dog::baby_name` 函数,它直接调用了定义于 `Dog` 之上的关联函数。这段代码会打印出:
|
||||
运行这段代码会打印出如下内容:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-19/output.txt}}
|
||||
```
|
||||
|
||||
这并不是我们需要的。我们希望调用的是 `Dog` 上 `Animal` trait 实现那部分的 `baby_name` 函数,这样能够打印出 `A baby dog is called a puppy`。示例 20-18 中用到的技术在这并不管用;如果将 `main` 改为示例 20-20 中的代码,则会得到一个编译错误:
|
||||
因为 `fly` 方法获取一个 `self` 参数,如果有两个**类型**都实现了同一 **trait**,Rust 可以根据 `self` 的类型计算出应该使用哪一个 trait 实现。
|
||||
|
||||
然而,关联函数中非方法的函数不带有 `self` 参数。当存在多个类型或者 trait 定义了相同函数名的非方法函数时,Rust 就不总是能计算出我们期望的是哪一个类型,除非使用 **完全限定语法**(*fully qualified syntax*)。例如示例 20-20 中的创建了一个希望将所有小狗叫做 *Spot* 的动物收容所的 trait。`Animal` trait 有一个关联非方法函数 `baby_name`。结构体 `Dog` 实现了 `Animal`,同时又直接提供了关联非方法函数 `baby_name`。
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust,ignore,does_not_compile
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-20/src/main.rs:here}}
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-20/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-20: 尝试调用 `Animal` trait 的 `baby_name` 函数,不过 Rust 并不知道该使用哪一个实现</span>
|
||||
<span class="caption">示例 20-20: 一个带有关联函数的 trait 和一个带有同名关联函数并实现了此 trait 的类型</span>
|
||||
|
||||
因为 `Animal::baby_name` 没有 `self` 参数,同时这可能会有其它类型实现了 `Animal` trait,Rust 无法计算出所需的是哪一个 `Animal::baby_name` 实现。我们会得到这个编译错误:
|
||||
在 `Dog` 类型上定义的关联函数 `baby_name` 中,我们实现了将所有小狗命名为 Spot 的功能。`Dog` 类型还实现了 `Animal` trait,它描述了所有动物所共有的特征。小狗被称为 puppy,这表现为 `Dog` 的 `Animal` trait 实现中与 `Animal` trait 相关联的函数 `baby_name`。
|
||||
|
||||
在 `main` 调用了 `Dog::baby_name` 函数,它直接调用了定义于 `Dog` 之上的关联函数。这段代码会打印出:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-20/output.txt}}
|
||||
```
|
||||
|
||||
为了消歧义并告诉 Rust 我们希望使用的是 `Dog` 的 `Animal` 实现而不是其它类型的 `Animal` 实现,需要使用 **完全限定语法**,这是调用函数时最为明确的方式。示例 20-21 展示了如何使用完全限定语法:
|
||||
这不是我们想要的输出。我们希望调用的是 `Dog` 上 `Animal` trait 实现那部分的 `baby_name` 函数,这样能够打印出 `A baby dog is called a puppy`。我们在示例 20-19 中使用的指定 trait 名称的技巧在这里不起作用;如果将 `main` 改为示例 20-21 中的代码,就会得到编译错误:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
```rust,ignore,does_not_compile
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-21/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-21: 使用完全限定语法来指定我们希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数</span>
|
||||
<span class="caption">示例 20-21: 尝试调用 `Animal` trait 的 `baby_name` 函数,不过 Rust 并不知道该使用哪一个实现</span>
|
||||
|
||||
我们在尖括号中向 Rust 提供了类型注解,并通过在此函数调用中将 `Dog` 类型当作 `Animal` 对待,来指定希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数。现在这段代码会打印出我们期望的数据:
|
||||
因为 `Animal::baby_name` 没有 `self` 参数,而且可能有其他类型实现了 `Animal` trait,Rust 无法确定我们想调用哪一个 `Animal::baby_name` 的实现。此时会得到如下编译错误:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-21/output.txt}}
|
||||
```
|
||||
|
||||
通常,完全限定语法定义为:
|
||||
为了消歧义并告诉 Rust 我们希望使用的是 `Dog` 的 `Animal` 实现而不是其它类型的 `Animal` 实现,需要使用**完全限定语法**。示例 20-22 演示了如何使用完全限定语法:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-22/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-22: 使用完全限定语法来指定我们希望调用的是 `Dog` 上 `Animal` trait 实现中的 `baby_name` 函数</span>
|
||||
|
||||
我们在尖括号中向 Rust 提供了类型注解,这表明我们希望在此次函数调用中将 `Dog` 类型视为 `Animal`,从而调用在 `Dog` 上实现的 `Animal` trait 中的 `baby_name` 方法。现在这段代码将打印出我们期望的结果:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch20-advanced-features/listing-20-22/output.txt}}
|
||||
```
|
||||
|
||||
通常,完全限定语法定义为如下:
|
||||
|
||||
```rust,ignore
|
||||
<Type as Trait>::function(receiver_if_method, next_arg, ...);
|
||||
```
|
||||
|
||||
对于不是方法的关联函数,其没有一个 `receiver`,故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
|
||||
对于不是方法的关联函数,并没有一个 `receiver`:故只会有其他参数的列表。可以选择在任何函数或方法调用处使用完全限定语法。然而,允许省略任何 Rust 能够从程序中的其他信息中计算出的部分。只有当存在多个同名实现而 Rust 需要帮助以便知道我们希望调用哪个实现时,才需要使用这个较为冗长的语法。
|
||||
|
||||
### 父 trait 用于在另一个 trait 中使用某 trait 的功能
|
||||
### 使用超 trait
|
||||
|
||||
有时我们可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的 **父(超)trait**(*supertrait*)。
|
||||
有时我们可能会需要编写一个依赖另一个 trait 的 trait 定义:对于一个实现了第一个 trait 的类型,你希望要求这个类型也实现了第二个 trait。如此就可使 trait 定义使用第二个 trait 的关联项。这个所需的 trait 是我们实现的 trait 的 **超(父)trait**(*supertrait*)。
|
||||
|
||||
例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会将给定的值格式化为带有星号框。也就是说,给定一个实现了标准库 `Display` trait 的并返回 `(x, y)` 的 `Point`,当调用以 `1` 作为 `x` 和 `3` 作为 `y` 的 `Point` 实例的 `outline_print` 会显示如下:
|
||||
例如我们希望创建一个带有 `outline_print` 方法的 trait `OutlinePrint`,它会将给定的值格式化为带有星号框。也就是说,给定一个实现了标准库 `Display` trait 的并返回 `(x, y)` 的 `Point`,当我们对一个 `x` 为 `1`、`y` 为 `3` 的 `Point` 实例调用 `outline_print` 时,它应该打印出如下内容:
|
||||
|
||||
```text
|
||||
**********
|
||||
@ -216,17 +215,17 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
|
||||
**********
|
||||
```
|
||||
|
||||
在 `outline_print` 的实现中,因为希望能够使用 `Display` trait 的功能,则需要说明 `OutlinePrint` 只能用于同时也实现了 `Display` 并提供了 `OutlinePrint` 需要的功能的类型。可以通过在 trait 定义中指定 `OutlinePrint: Display` 来做到这一点。这类似于为 trait 增加 trait bound。示例 20-22 展示了一个 `OutlinePrint` trait 的实现:
|
||||
在 `outline_print` 的实现中,我们希望使用 `Display` trait 的功能。因此,需要说明 `OutlinePrint` trait 仅适用于那些同时实现了 `Display` 并提供 `OutlinePrint` 所需功能的类型。可以通过在 trait 定义中指定 `OutlinePrint: Display` 来做到这一点。这种技术类似于为 trait 增加 trait bound。示例 20-23 展示了一个 `OutlinePrint` trait 的实现:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-22/src/main.rs:here}}
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-23/src/main.rs:here}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-22: 实现 `OutlinePrint` trait,它要求来自 `Display` 的功能</span>
|
||||
<span class="caption">示例 20-23: 实现 `OutlinePrint` trait,它要求来自 `Display` 的功能</span>
|
||||
|
||||
因为指定了 `OutlinePrint` 需要 `Display` trait,则可以在 `outline_print` 中使用 `to_string`,其会为任何实现 `Display` 的类型自动实现。如果不在 trait 名后增加 `: Display` 并尝试在 `outline_print` 中使用 `to_string`,则会得到一个错误说在当前作用域中没有找到用于 `&Self` 类型的方法 `to_string`。
|
||||
因为我们已经指定 `OutlinePrint` 需要 `Display` trait,因而可以使用自动为任何实现了 `Display` 的类型提供的 `to_string` 方法。如果我们在没有在 trait 名称后添加冒号并指定 `Display` trait 的情况下尝试使用 `to_string`,就会出现错误,提示在当前作用域中未为类型 `&Self` 找到名为 `to_string` 的方法。
|
||||
|
||||
让我们看看如果尝试在一个没有实现 `Display` 的类型上实现 `OutlinePrint` 会发生什么,比如 `Point` 结构体:
|
||||
|
||||
@ -242,7 +241,7 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
|
||||
{{#include ../listings/ch20-advanced-features/no-listing-02-impl-outlineprint-for-point/output.txt}}
|
||||
```
|
||||
|
||||
一旦在 `Point` 上实现 `Display` 并满足 `OutlinePrint` 要求的限制,比如这样:
|
||||
为了修复这个问题,我们在 `Point` 上实现 `Display` 并满足 `OutlinePrint` 要求的限制,比如这样:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -250,29 +249,29 @@ Rust 既不能避免一个 trait 与另一个 trait 拥有相同名称的方法
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-03-impl-display-for-point/src/main.rs:here}}
|
||||
```
|
||||
|
||||
那么在 `Point` 上实现 `OutlinePrint` trait 将能成功编译,并可以在 `Point` 实例上调用 `outline_print` 来显示位于星号框中的点的值。
|
||||
那么在 `Point` 上实现 `OutlinePrint` trait 就能成功编译,并可以在 `Point` 实例上调用 `outline_print` 将其显示在由星号组成的边框内。
|
||||
|
||||
### newtype 模式用以在外部类型上实现外部 trait
|
||||
### 使用 newtype 模式在外部类型上实现外部 trait
|
||||
|
||||
在第十章的 [“为类型实现 trait”][implementing-a-trait-on-a-type] 部分,我们提到了孤儿规则(orphan rule),它说明只有 trait 或类型对于当前 crate 是本地时,才可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 **newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第五章 [“用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。接着这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。*Newtype* 是一个源自 Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。
|
||||
在第十章的 [“为类型实现 trait”][implementing-a-trait-on-a-type] 部分,我们提到了孤儿规则(orphan rule),它规定只有当 trait 或类型至少有一方或两者都对于当前 crate 是本地时,才能在该类型上实现该 trait。一个绕开这个限制的方法是使用 **newtype 模式**(*newtype pattern*),它涉及到在一个元组结构体(第五章 [“用没有命名字段的元组结构体来创建不同的类型”][tuple-structs] 部分介绍了元组结构体)中创建一个新类型。这个元组结构体带有一个字段作为希望实现 trait 的类型的简单封装。由于这个封装类型对于 crate 是本地的,这样就可以在这个封装上实现 trait。*Newtype* 是一个源自 Haskell 编程语言的概念。使用这个模式没有运行时性能惩罚,这个封装类型在编译时就被省略了。
|
||||
|
||||
例如,如果想要在 `Vec<T>` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec<T>` 都定义于我们的 crate 之外。可以创建一个包含 `Vec<T>` 实例的 `Wrapper` 结构体,接着可以如列表 20-23 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec<T>` 的值:
|
||||
例如,如果想要在 `Vec<T>` 上实现 `Display`,而孤儿规则阻止我们直接这么做,因为 `Display` trait 和 `Vec<T>` 都定义于我们的 crate 之外。可以创建一个包含 `Vec<T>` 实例的 `Wrapper` 结构体,接着可以如列表 20-24 那样在 `Wrapper` 上实现 `Display` 并使用 `Vec<T>` 的值:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-23/src/main.rs}}
|
||||
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-24/src/main.rs}}
|
||||
```
|
||||
|
||||
<span class="caption">示例 20-23: 创建 `Wrapper` 类型封装 `Vec<String>` 以便能够实现 `Display`</span>
|
||||
<span class="caption">示例 20-24: 创建 `Wrapper` 类型封装 `Vec<String>` 以便能够实现 `Display`</span>
|
||||
|
||||
`Display` 的实现使用 `self.0` 来访问其内部的 `Vec<T>`,因为 `Wrapper` 是元组结构体而 `Vec<T>` 是结构体总位于索引 0 的项。接着就可以使用 `Wrapper` 中 `Display` 的功能了。
|
||||
|
||||
此方法的缺点是,因为 `Wrapper` 是一个新类型,它没有定义于其值之上的方法;必须直接在 `Wrapper` 上实现 `Vec<T>` 的所有方法,这样就可以代理到`self.0` 上 —— 这就允许我们完全像 `Vec<T>` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则必须只自行实现所需的方法。
|
||||
这种做法的缺点在于因为 `Wrapper` 是一个新类型,它并不具备其所封装值的方法。必须直接在 `Wrapper` 上实现 `Vec<T>` 的所有方法,这样就可以代理到`self.0` 上,这就允许我们完全像 `Vec<T>` 那样对待 `Wrapper`。如果希望新类型拥有其内部类型的每一个方法,为封装类型实现 `Deref` trait(第十五章 [“通过 `Deref` trait 将智能指针当作常规引用处理”][smart-pointer-deref] 部分讨论过)并返回其内部类型是一种解决方案。如果不希望封装类型拥有所有内部类型的方法 —— 比如为了限制封装类型的行为 —— 则只需自行实现所需的方法即可。
|
||||
|
||||
甚至当不涉及 trait 时 newtype 模式也很有用。现在让我们将话题的焦点转移到一些与 Rust 类型系统交互的高级方式上来吧。
|
||||
甚至当不涉及 trait 时 newtype 模式也很有用。现在让我们将关注点转移到一些与 Rust 类型系统交互的高级方式上来吧。
|
||||
|
||||
[newtype]: ch20-02-advanced-traits.html#newtype-模式用以在外部类型上实现外部-trait
|
||||
[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-将某类型像引用一样处理
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名(type aliases),一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。
|
||||
|
||||
> 这一部分假设你已经阅读了之前的 [“newtype 模式用于在外部类型上实现外部 trait”][using-the-newtype-pattern] 部分。
|
||||
> 这一部分假设你已经阅读了之前的 [“使用 newtype 模式在外部类型上实现外部 trait”][using-the-newtype-pattern] 部分。
|
||||
|
||||
### 为了类型安全和抽象而使用 newtype 模式
|
||||
|
||||
@ -169,4 +169,4 @@ ch18-01-what-is-oo.html#封装隐藏了实现细节
|
||||
ch06-02-match.html#match-控制流结构
|
||||
[using-trait-objects-that-allow-for-values-of-different-types]:
|
||||
ch18-02-trait-objects.html#顾及不同类型值的-trait-对象
|
||||
[using-the-newtype-pattern]: ch20-02-advanced-traits.html#newtype-模式用以在外部类型上实现外部-trait
|
||||
[using-the-newtype-pattern]: ch20-02-advanced-traits.html#使用-newtype-模式在外部类型上实现外部-trait
|
||||
|
Loading…
Reference in New Issue
Block a user