Compare commits

...

2 Commits

Author SHA1 Message Date
KaiserY
2afe112c1b update ch20-04 2025-05-10 14:09:56 +08:00
KaiserY
226a1804fd update ch20-03 2025-05-10 11:13:52 +08:00
4 changed files with 120 additions and 73 deletions

View File

@ -1,4 +1,4 @@
# Rust 程序设计语言2021 edition简体中文版
# Rust 程序设计语言2024 edition简体中文版
![Build Status](https://github.com/KaiserY/trpl-zh-cn/workflows/CI/badge.svg)

View File

@ -2,35 +2,35 @@
> [ch20-03-advanced-types.md](https://github.com/rust-lang/book/blob/main/src/ch20-03-advanced-types.md)
> <br>
> commit 95e931170404cb98d476b19017cbbdbc00d0834d
> commit 56ec353290429e6547109e88afea4de027b0f1a9
Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名type aliases一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。
Rust 的类型系统有一些我们曾经提到但尚未讨论过的特性。首先我们将从一般意义上讨论 newtype 并探讨它们作为类型为何有用。接着会转向类型别名type aliases一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。
> 这一部分假设你已经阅读了之前的 [“使用 newtype 模式在外部类型上实现外部 trait”][using-the-newtype-pattern] 部分。
### 使用 newtype 模式实现类型安全和抽象
### 为了类型安全和抽象而使用 newtype 模式
本小节假设你已经阅读了之前的 [“使用 newtype 模式在外部类型上实现外部 trait”][using-the-newtype-pattern] 部分。
newtype 模式也可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单位。实际上示例 19-15 中已经有一个这样的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。
newtype 模式还可用于我们到目前为止尚未讨论的其他任务,包括静态地确保值不会混淆以及标注值的单位。你在示例 20-16 中已经看到了一个使用 newtype 来表示单位的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。
newtype 模式也可以用于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API。
newtype 模式也可以用于抽象掉某个类型的部分实现细节:新的类型可以暴露与其私有内部类型不同的共有 API。
newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap<i32, String>``People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十八章 [“封装隐藏了实现细节”][encapsulation-that-hides-implementation-details] 部分所讨论的隐藏实现细节的封装的轻量级方法。
newtype 模式还可以隐藏内部实现。例如,可以提供一个封装了 `HashMap<i32, String>``People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与我们提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十八章 [“封装隐藏了实现细节”][encapsulation-that-hides-implementation-details] 中讨论的隐藏实现细节的轻量级封装方法。
### 类型别名用来创建类型同义词
### 使用类型别名创建类型同义词
Rust 提供了声明 **类型别名***type alias*)的能力,使用 `type` 关键字来给予现有类型另一个名字。例如,可以像这样创建 `i32` 的别名 `Kilometers`
Rust 提供了声明 **类型别名***type alias*)的能力,使用 `type` 关键字为现有类型赋予另一个名字。例如,可以像这样创建 `i32` 的别名 `Kilometers`
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:here}}
```
这意味着 `Kilometers``i32`**同义词***synonym*);不同于示例 19-15 中创建的 `Millimeters``Meters` 类型。`Kilometers` 不是一个新的、单独的类型。`Kilometers` 类型的值将被完全当作 `i32` 类型值来对待:
这意味着 `Kilometers``i32`**同义词***synonym*);不同于示例 20-16 中创建的 `Millimeters``Meters` 类型。`Kilometers` 不是一个新的、单独的类型。`Kilometers` 类型的值将被完全当作 `i32` 类型值来对待:
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-04-kilometers-alias/src/main.rs:there}}
```
因为 `Kilometers``i32` 的别名,它们是同一类型,可以将 `i32``Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在哪里混用 `Kilometers``i32` 的值,编译器也不会给出一个错误。
因为 `Kilometers``i32` 的别名,它们是同一类型,可以将 `i32``Kilometers` 相加,也可以将 `Kilometers` 传递给获取 `i32` 参数的函数。但通过这种手段无法获得上一部分讨论的 newtype 模式所提供的类型检查的好处。换句话说,如果在某处混用 `Kilometers``i32` 的值,编译器也不会给出一个错误。
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
@ -38,61 +38,61 @@ Rust 提供了声明 **类型别名***type alias*)的能力,使用 `type`
Box<dyn Fn() + Send + 'static>
```
在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-24 这样全是如此代码的项目:
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-24/src/main.rs:here}}
```
<span class="caption">示例 19-24: 在很多地方使用名称很长的类型</span>
类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个叫做 `Thunk` 的别名,这样就可以如示例 19-25 所示将所有使用这个类型的地方替换为更短的 `Thunk`
在函数签名和类型注解中到处书写这个冗长的类型既乏味又容易出错。想象一下有一个项目,到处都是像 Listing 20-25 那样的代码。
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-25/src/main.rs:here}}
```
<span class="caption">示例 19-25: 引入类型别名 `Thunk` 来减少重复</span>
<span class="caption">示例 20-25: 在很多地方使用名称很长的类型</span>
这样读写起来就容易多了!为类型别名选择一个好名字也可以帮助你表达意图(单词 *thunk* 表示会在之后被计算的代码,所以这是一个存放闭包的合适的名字)
类型别名通过减少重复使代码更易于管理。在示例 20-26 中,我们为这个冗长的类型引入了名为 `Thunk` 的别名,并可以使用更简洁的 `Thunk` 来替换所有使用该类型的地方。
类型别名也经常与 `Result<T, E>` 结合使用来减少重复。考虑一下标准库中的 `std::io` 模块。I/O 操作通常会返回一个 `Result<T, E>`,因为这些操作可能会失败。标准库中的 `std::io::Error` 结构体代表了所有可能的 I/O 错误。`std::io` 中大部分函数会返回 `Result<T, E>`,其中 `E``std::io::Error`,比如 `Write` trait 中的这些函数:
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-26/src/main.rs:here}}
```
<span class="caption">示例 20-26: 引入类型别名 `Thunk` 来减少重复</span>
这样阅读和编写代码都容易多了!为类型别名选择一个好名字也可以帮助你表达意图(单词 *thunk* 表示会在之后被计算的代码,所以这是一个存放闭包的合适的名字)。
类型别名也经常与 `Result<T, E>` 结合使用来减少重复。考虑一下标准库中的 `std::io` 模块。I/O 操作通常会返回一个 `Result<T, E>` 来处理操作失败的情况。标准库中的 `std::io::Error` 结构体代表了所有可能的 I/O 错误。`std::io` 中的许多函数都会返回 `Result<T, E>`,其中 `E``std::io::Error`,比如 `Write` trait 中的这些函数:
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-05-write-trait/src/lib.rs}}
```
这里出现了很多的 `Result<..., Error>`。为此,`std::io` 有这个类型别名声明:
这里重复出现了很多次 `Result<..., Error>`。为此,`std::io` 有这个类型别名声明:
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:here}}
```
因为这位于 `std::io` 中,可用的完全限定的别名是 `std::io::Result<T>`;也就是说,`Result<T, E>` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样:
该声明位于 `std::io` 模块中,因此我们可以使用完全限定的别名 `std::io::Result<T>`;也就是说,`Result<T, E>` 中 `E` 放入了 `std::io::Error`。`Write` trait 中的函数最终看起来像这样:
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-06-result-alias/src/lib.rs:there}}
```
类型别名在两个方面有帮助:易于编写 **并** 在整个 `std::io` 中提供了一致的接口。因为这是一个别名,它只是另一个 `Result<T, E>`,这意味着可以在其上使用 `Result<T, E>` 的任何方法,以及像 `?` 这样的特殊语法。
类型别名在两个方面有帮助:易于编写**并**在整个 `std::io` 中提供了一致的接口。因为这是一个别名,它只是另一个 `Result<T, E>`,这意味着可以在其上使用 `Result<T, E>` 的任何方法,以及像 `?` 这样的特殊语法。
### 从不返回的 never type
Rust 有一个叫做 `!` 的特殊类型。在类型理论术语中,它被称为 *empty type*,因为它没有值。我们更倾向于称之为 *never type*。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例
Rust 有一个叫做 `!` 的特殊类型。在类型理论术语中被称为 *empty type*,因为它没有值。我们更倾向于称之为 *never type*。这个名字描述了它的作用:在函数从不返回的时候充当返回值。下面是一个示例:
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-07-never-type/src/lib.rs:here}}
```
这读 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数***diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回值。
段代码可以 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数***diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回值。
不过一个不能创建值的类型有什么用呢?如果你回想一下示例 2-5 中的代码,曾经有一些看起来像这样的代码,如示例 19-26 所重现的
不过一个不能创建值的类型有什么用呢?回想一下示例 2-5 中猜数字游戏的代码;我们在示例 20-27 中重现了其中的一小部分
```rust,ignore
{{#rustdoc_include ../listings/ch02-guessing-game-tutorial/listing-02-05/src/main.rs:ch19}}
```
<span class="caption">示例 19-26: `match` 语句和一个以 `continue` 结束的分支</span>
<span class="caption">示例 20-27: `match` 语句和一个以 `continue` 结束的分支</span>
当时我们忽略了代码中的一些细节。在第六章 [“`match` 控制流运算符”][the-match-control-flow-operator] 部分,我们学习了 `match` 的分支必须返回相同的类型。如下代码不能工作:
@ -100,19 +100,19 @@ Rust 有一个叫做 `!` 的特殊类型。在类型理论术语中,它被称
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-08-match-arms-different-types/src/main.rs:here}}
```
这里的 `guess` 必须既是整型 **也是** 字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么示例 19-26 中会允许一个分支返回 `u32` 而另一个分支却以 `continue` 结束呢?
这里的 `guess` 必须既是整型 **也是** 字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么在示例 20-27 中,一个分支返回 `u32`而另一个分支却以 `continue` 结束呢?
正如你可能猜到的,`continue` 的值是 `!`。也就是说,当 Rust 要计算 `guess` 的类型时,它查看这两个分支。前者是 `u32` 值,而后者是 `!` 值。因为 `!` 并没有一个Rust 决定 `guess` 的类型是 `u32`
正如你可能猜到的,`continue` 的值是 `!`。也就是说,当 Rust 要计算 `guess` 的类型时,它查看这两个分支。前者是 `u32` 值,而后者是 `!` 值。因为 `!` 类型永远不会有Rust 决定 `guess` 的类型是 `u32`
描述 `!` 的行为的正式方式是 never type 可以强转为任何其他类型。允许 `match`分支以 `continue` 结束是因为 `continue` 并不真正返回一个值;相反它把控制权交回上层循环,所以在 `Err` 的情况,事实上并未对 `guess` 赋值。
描述这种行为的正式方式是,类型为 `!` 的表达式可以被强制转换为任意其他类型。之所以允许 `match` 分支以 `continue` 结束是因为 `continue` 并不真正返回值;相反它把控制权交回上层循环,所以在 `Err` 的情况,事实上并未对 `guess` 进行赋值。
never type 的另一个用途是 `panic!`。还记得 `Option<T>` 上的 `unwrap` 函数吗?它产生一个值或 panic。这里是它的定义
never type `panic!` 宏中也很有用。还记得 `Option<T>` 上的 `unwrap` 函数吗?它产生一个值或 panic。这里是它的定义
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-09-unwrap-definition/src/lib.rs:here}}
```
这里与示例 19-34 中的 `match` 发生了相同的情况Rust 知道 `val``T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效的。
这里与示例 20-27 中的 `match` 发生了相同的情况Rust 知道 `val``T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效的。
最后一个有着 `!` 类型的表达式是 `loop`
@ -124,43 +124,43 @@ never type 的另一个用途是 `panic!`。还记得 `Option<T>` 上的 `unwrap
### 动态大小类型和 `Sized` trait
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 **动态大小类型***dynamically sized types*)。这有时被称为 “DST” 或 “unsized types”这些类型允许我们处理只有在运行时才知道大小的类型
Rust 需要知道有关类型的某些细节,例如为特定类型的值需要分配多少空间。这便是起初留下的一个类型系统中令人迷惑的角落:即 **动态大小类型***dynamically sized types*的概念。这有时被称为 “DST” 或 “unsized types”它们让我们能够编写使用那些只有在运行时才能知道大小的值的代码
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道其大小也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,它们不能工作:
让我们深入研究我们在整本书中一直在使用的动态大小类型 `str` 的细节。没错,不是 `&str`,而是单独的 `str` 就是一个 DST。直到运行时我们都不知道字符串有多长。我们无法在编译时知道字符串的长度这意味着我们无法创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,它们不能工作:
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-11-cant-create-str/src/main.rs:here}}
```
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间。不过它们有着不同的长度:`s1` 需要 12 字节存储,而 `s2` 需要 15 字节。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 部分slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是 **两个** 值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,我们总是知道 `&str` 的大小,而无论其引用的字符串是多长。这里是 Rust 中动态大小类型的常规用法:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金则:必须将动态大小类型的值置于某种指针之后。
那么该怎么办呢?在这种情况下,你已经知道答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 [“字符串 slice”][string-slices] 中提到slice 数据结构仅仅储存了开始位置和 slice 的长度。所以虽然 `&T` 是一个储存了 `T` 所在的内存位置的单个值,`&str` 则是**两个**值:`str` 的地址和其长度。这样,`&str` 就有了一个在编译时可以知道的大小:它是 `usize` 长度的两倍。也就是说,无论所引用的字符串多长,我们总是知道 `&str` 的大小。一般来说,这就是 Rust 使用动态大小类型的方式:它们有一些额外的元信息来储存动态信息的大小。这引出了动态大小类型的黄金则:必须将动态大小类型的值置于某种指针之后。
可以将 `str` 与所有类型的指针结合:比如 `Box<str>``Rc<str>`。事实上之前我们已经见过了不过是另一个动态大小类型trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十八章 [顾及不同类型值的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分,我们提到了为了将 trait 用于 trait 对象,必须将它们放入指针之后,比如 `&dyn Trait``Box<dyn Trait>``Rc<dyn Trait>` 也可以)。
可以将 `str` 与所有类型的指针结合:比如 `Box<str>``Rc<str>`。事实上之前我们已经见过了不过是另一个动态大小类型trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十八章 [顾及不同类型值的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] ,我们提到了为了将 trait 用于 trait 对象,必须将它们放入指针之后,比如 `&dyn Trait``Box<dyn Trait>``Rc<dyn Trait>` 也可以)。
为了处理 DSTRust 提供了 `Sized` trait 来决定一个类型的大小是否在编译时可知。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说对于如下泛型函数定义
为了处理 DSTRust 提供了 `Sized` trait 来决定一个类型的大小是否在编译时可知。该 trait 会自动为所有在编译时大小已知的类型实现。此外Rust 隐式地为每一个泛型函数增加了 `Sized` bound。也就是说对于如下泛型函数定义
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-12-generic-fn-definition/src/lib.rs}}
```
实际上被当作如下处理:
实际上,这会被当作我们写了如下内容来处理:
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-13-generic-implicit-sized-bound/src/lib.rs}}
```
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制:
默认情况下,泛型函数只能作用于在编译时大小已知的类型。然而,你可以使用如下特殊语法来放宽这一限制:
```rust,ignore
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-14-generic-maybe-sized/src/lib.rs}}
```
`?Sized` 上的 trait bound 意味着 “`T` 可能是也可能不是 `Sized`” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。这种意义的 `?Trait` 语法只能用于 `Sized` ,而不能用于任何其他 trait。
`?Sized` 这个 trait bound 表示 “`T` 可以是 `Sized`,也可以不是 `Sized`” 同时这个注解会覆盖泛型类型必须在编译时拥有固定大小的默认规则。具有该含义的 `?Trait` 语法仅适用于 `Sized`,而不适用于其他任何 trait。
另外注意我们将 `t` 参数的类型从 `T` 变为了 `&T`:因为其类型可能不是 `Sized` 的,所以需要将其置于某种指针之后。在这个例子中选择了引用。
接下来,我们讨论一下函数和闭包!
接下来,我们讨论函数和闭包!
[encapsulation-that-hides-implementation-details]:
ch18-01-what-is-oo.html#封装隐藏了实现细节

View File

@ -2,80 +2,127 @@
> [ch20-04-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/main/src/ch20-04-advanced-functions-and-closures.md)
> <br>
> commit 21cf840842bdf768a798869f06373c96c1cc5122
> commit 56ec353290429e6547109e88afea4de027b0f1a9
本部分将探索一些有关函数和闭包的高级功能,这包括函数指针以及返回值闭包。
本部分将探索一些有关函数和闭包的高级特性,这包括函数指针以及返回闭包。
### 函数指针
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数满足类型 `fn`(小写的 f不要与闭包 trait 的 `Fn` 相混淆。`fn` 被称为 **函数指针***function pointer*)。通过函数指针允许我们使用函数作为另一个函数的参数。
我们讨论过了如何向函数传递闭包;也可以将普通函数传递给函数!这个技术在我们希望传递已经定义的函数而不是重新定义闭包作为参数时很有用。函数会被强制转换为 `fn` 类型(小写的 f不要与闭包 trait 的 `Fn` 相混淆。`fn` 被称为 **函数指针***function pointer*)。通过函数指针允许我们使用函数作为其它函数的参数。
指定参数为函数指针的语法类似于闭包,如示例 19-27 所示,这里定义了一个 `add_one` 函数将其参数加一。`do_twice` 函数获取两个参数:一个指向任何获取一个 `i32` 参数并返回一个 `i32` 的函数指针,和一个 `i32` 值。`do_twice` 函数传 `arg` 参数调用 `f` 函数两次,接着将两次函数调用的结果相加。`main` 函数使用 `add_one``5` 作为参数调用 `do_twice`
指定参数为函数指针的语法类似于闭包,如示例 20-28 所示,这里定义了一个 `add_one` 函数用于将其参数加一。`do_twice` 函数获取两个参数:一个指向任何获取一个 `i32` 参数并返回一个 `i32` 的函数指针,和一个 `i32` 值。`do_twice` 函数传 `arg` 参数调用 `f` 函数两次,接着将两次函数调用的结果相加。`main` 函数使用 `add_one``5` 作为参数调用 `do_twice`
<span class="filename">文件名src/main.rs</span>
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-27/src/main.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-28/src/main.rs}}
```
<span class="caption">示例 19-27: 使用 `fn` 类型接受函数指针作为参数</span>
<span class="caption">示例 20-28: 使用 `fn` 类型接受函数指针作为参数</span>
这会打印出 `The answer is: 12`。`do_twice` 中的 `f` 被指定为一个接受一个 `i32` 参数并返回 `i32``fn`。接着就可以在 `do_twice` 函数体中调用 `f`。在 `main` 中,可以将函数名 `add_one` 作为第一个参数传递给 `do_twice`
段代码会打印出 `The answer is: 12`。`do_twice` 中的 `f` 被指定为一个接受一个 `i32` 参数并返回 `i32``fn`。接着就可以在 `do_twice` 函数体中调用 `f`。在 `main` 中,可以将函数名 `add_one` 作为第一个参数传递给 `do_twice`
不同于闭包,`fn` 是一个类型而不是一个 trait所以直接指定 `fn` 作为参数而不是声明一个带有 `Fn` 作为 trait bound 的泛型参数。
函数指针实现了所有三个闭包 trait`Fn`、`FnMut` 和 `FnOnce`),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。
一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
尽管如此,一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 `map` 的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector就可以使用闭包比如这样
作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个标准库中 `Iterator` trait 提供的 `map` 方法的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector就可以使用闭包如示例 20-29 所示:
<figure class="listing">
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-15-map-closure/src/main.rs:here}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-29/src/main.rs:here}}
```
或者可以将函数作为 `map` 的参数来代替闭包,像是这样:
<figcaption>示例 20-29使用闭包和 `map` 方法将数字转换为字符串</figcaption>
</figure>
或者可以将函数作为 `map` 的参数来代替闭包,如示例 20-30 所示:
<figure class="listing">
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-16-map-function/src/main.rs:here}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-30/src/main.rs:here}}
```
注意这里必须使用 [“高级 trait”][advanced-traits] 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数;这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。
<figcaption>示例 20-30使用 `String::to_string` 方法将数字转换为字符串</figcaption>
回忆一下第六章 [“枚举值”][enum-values] 部分中定义的每一个枚举成员也变成了一个构造函数。我们可以使用这些构造函数作为实现了闭包 trait 的函数指针,这意味着可以指定构造函数作为接受闭包的方法的参数,如下:
</figure>
注意这里必须使用 [“高级 trait”][advanced-traits] 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数。
这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。
回忆一下第六章 [“枚举值”][enum-values] 部分中定义的每一个枚举成员也变成了一个构造函数。我们可以使用这些构造函数作为实现了闭包 trait 的函数指针,这意味着可以指定构造函数作为接受闭包的方法的参数,如示例 20-31 所示:
<figure class="listing">
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-17-map-initializer/src/main.rs:here}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-31/src/main.rs:here}}
```
这里创建了 `Status::Value` 实例,它通过 `map` 用范围的每一个 `u32` 值调用 `Status::Value` 的初始化函数。一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。
<figcaption>示例 20-31使用枚举构造函数和 `map` 方法从数字创建 `Status` 实例</figcaption>
</figure>
这里,我们通过 `Status::Value` 的初始化函数,对 `map` 所作用的范围内每个 `u32` 值创建 `Status::Value` 实例。一些人倾向于函数式风格,一些人喜欢闭包。它们会编译成相同的代码,因此请选择对你来说更清晰的那一种。
### 返回闭包
闭包表现为 trait这意味着不能直接返回闭包。对于大部分需要返回 trait 的情况,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如不允许使用函数指针 `fn` 作为返回值类型。
闭包表现为 trait这意味着不能直接返回闭包。对于大部分需要返回 trait 的场景中,可以使用实现了期望返回的 trait 的具体类型来替代函数的返回值。但是这不能用于闭包,因为它们没有一个可返回的具体类型;例如,当闭包从其作用域捕获任何值时,就不允许使用函数指针 `fn` 作为返回类型。
这段代码尝试直接返回闭包,它并不能编译:
相反,可以正常地使用第十章所学的 `impl Trait` 语法。可以使用 `Fn`、`FnOnce` 和 `FnMut` 返回任何函数类型。例如,示例 20-32 中的代码就可以正常工作。
<figure class="listing">
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-32/src/lib.rs}}
```
<figcaption>示例 20-32使用 `impl Trait` 语法从函数返回闭包</figcaption>
</figure>
然而,如我们在 [“闭包类型推断和注解”][closure-types] 中所注意到的,每一个闭包也有其独立的类型。如果你需要处理多个拥有相同签名但是不同实现的函数,就需要使用 trait 对象。考虑一下如果编写类似示例 20-33 中所示代码会发生什么。
<figure class="listing">
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-18-returns-closure/src/lib.rs}}
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-33/src/main.rs}}
```
编译器给出的错误是:
<figcaption>示例 20-33创建一个由返回 `impl Fn` 的函数定义的闭包的 `Vec<T>`</figcaption>
```console
{{#include ../listings/ch20-advanced-features/no-listing-18-returns-closure/output.txt}}
</figure>
这里有两个函数,`returns_closure` 和 `returns_initialized_closure`,它们都返回 `impl Fn(i32) -> i32`。注意它们返回的闭包是不同的即使它们实现了相同的类型。如果尝试编译这段代码Rust 会告诉我们这不可行:
```text
{{#include ../listings/ch20-advanced-features/listing-20-33/output.txt}}
```
错误又一次指向了 `Sized` traitRust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:
错误信息告诉我们每当返回一个 `impl Trait` Rust 会创建一个独特的**不透明类型***opaque type*),这是一个无法看清 Rust 为我们构建了什么细节的类型。所以即使这些函数都返回了实现了相同 trait `Fn(i32) -> i32`的闭包Rust 为我们生成的不透明类型也是不同的。这类似于 Rust 如何为不同的异步代码块生成不同的具体类型,即使它们有着相同的输出类型,如第十七章 [“使用任意数量的 futures”][any-number-of-futures] 所示。我们已经多次看到这个问题的解决方案:我们可以使用 trait 对象,如示例 20-34 所示。
```rust,noplayground
{{#rustdoc_include ../listings/ch20-advanced-features/no-listing-19-returns-closure-trait-object/src/lib.rs}}
<figure class="listing">
```rust
{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-34/src/main.rs:here}}
```
<figcaption>示例 20-34创建一个由返回 `Box<dyn Fn>` 的函数定义的闭包的 `Vec<T>` 以便它们有相同的类型</figcaption>
</figure>
这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十八章的 [顾及不同类型值的 trait 对象”][using-trait-objects-that-allow-for-values-of-different-types] 部分。
接下来让我们学习宏!
[advanced-traits]: ch20-02-advanced-traits.html#高级-trait
[enum-values]: ch06-01-defining-an-enum.html#枚举值
[closure-types]: ch13-01-closures.html#闭包类型推断和注解
[any-number-of-futures]: ch17-03-more-futures.html
[using-trait-objects-that-allow-for-values-of-different-types]: ch18-02-trait-objects.html#顾及不同类型值的-trait-对象

View File

@ -302,7 +302,7 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
#### 实现 `execute` 方法
最后让我们实现 `ThreadPool` 上的 `execute` 方法。同时也要修改 `Job` 结构体:它将不再是结构体,`Job` 将是一个有着 `execute` 接收到的闭包类型的 trait 对象的类型别名。第二十章 [“类型别名用来创建类型同义词”][creating-type-synonyms-with-type-aliases] 部分提到过,类型别名允许将长的类型变短。观察示例 20-19
最后让我们实现 `ThreadPool` 上的 `execute` 方法。同时也要修改 `Job` 结构体:它将不再是结构体,`Job` 将是一个有着 `execute` 接收到的闭包类型的 trait 对象的类型别名。第二十章 [“使用类型别名创建类型同义词”][creating-type-synonyms-with-type-aliases] 部分提到过,类型别名允许将长的类型变短。观察示例 20-19
<span class="filename">文件名src/lib.rs</span>
@ -389,7 +389,7 @@ Worker 2 got a job; executing.
示例 20-20 中的代码使用的 `let job = receiver.lock().unwrap().recv().unwrap();` 之所以可以工作是因为对于 `let` 来说,当 `let` 语句结束时任何表达式中等号右侧使用的临时值都会立即被丢弃。然而 `while let``if let` 和 `match`)直到相关的代码块结束都不会丢弃临时值。在示例 20-21 中,`job()` 调用期间锁一直持续,这也意味着其他的 worker 无法接受任务。
[creating-type-synonyms-with-type-aliases]:
ch20-03-advanced-types.html#类型别名用来创建类型同义词
ch20-03-advanced-types.html#使用类型别名创建类型同义词
[integer-types]: ch03-02-data-types.html#整型
[fn-traits]: ch13-01-closures.html#将被捕获的值移出闭包和-fn-trait
[builder]: https://doc.rust-lang.org/std/thread/struct.Builder.html