check to apendix-03

This commit is contained in:
KaiserY 2018-12-09 23:22:10 +08:00
parent 50d8b51689
commit 36293d02bc
18 changed files with 680 additions and 382 deletions

View File

@ -1,10 +1,10 @@
# Rust 程序设计语言(第二版) 简体中文版
# Rust 程序设计语言(第二版 & 2018 edition 简体中文版
[![Build Status](https://travis-ci.org/KaiserY/trpl-zh-cn.svg?branch=master)](https://travis-ci.org/KaiserY/trpl-zh-cn)
## 状态
目前正向 2018 edtion 过渡,目前官方仓库状已经不再分版本提供源码了,故本仓库准备与官方保持一致。已更新到第十七章。
2018 edition 的翻译迁移已基本完成,欢迎阅读学习!
PS:
@ -17,15 +17,15 @@ PS:
### 构建
你可以将本mdbook构建成一系列静态html页面。这里我们采用[vuepress](https://vuepress.vuejs.org/zh/)打包出静态网页。在这之前,你需要安装[Nodejs](https://nodejs.org/zh-cn/)。
你可以将本 mdbook 构建成一系列静态 html 页面。这里我们采用 [vuepress](https://vuepress.vuejs.org/zh/) 打包出静态网页。在这之前,你需要安装 [Nodejs](https://nodejs.org/zh-cn/)。
全局安装vuepress
全局安装 vuepress
``` bash
npm i -g vuepress
```
cd到项目目录然后开始构建。构建好的静态文档会出现在"./src/.vuepress/dist"中
cd 到项目目录,然后开始构建。构建好的静态文档会出现在 "./src/.vuepress/dist"
```bash
vuepress build ./src
@ -33,7 +33,7 @@ vuepress build ./src
### 文档撰写
vuepress会启动一个本地服务器并在浏览器对你保存的文档进行实时热更新。
vuepress 会启动一个本地服务器,并在浏览器对你保存的文档进行实时热更新。
```bash
vuepress dev ./src

View File

@ -1 +1 @@
# Rust 程序设计语言(第二版 简体中文版
# Rust 程序设计语言(第二版 & 2018 edition简体中文版

View File

@ -115,6 +115,7 @@
- [高级 trait](ch19-03-advanced-traits.md)
- [高级类型](ch19-04-advanced-types.md)
- [高级函数与闭包](ch19-05-advanced-functions-and-closures.md)
- [](ch19-06-macros.md)
- [最后的项目: 构建多线程 web server](ch20-00-final-project-a-web-server.md)
- [单线程 web server](ch20-01-single-threaded.md)
@ -125,7 +126,7 @@
- [A - 关键字](appendix-01-keywords.md)
- [B - 运算符与符号](appendix-02-operators.md)
- [C - 可派生的 trait](appendix-03-derivable-traits.md)
- [D - 宏](appendix-04-macros.md)
- [E - 本书翻译](appendix-05-translation.md)
- [F - 最新功能](appendix-06-newest-features.md)
- [D - 实用开发工具](appendix-04-useful-development-tools.md)
- [E - 版本](appendix-05-editions.md)
- [F - 本书译本](appendix-06-translation.md)
- [G - Rust 是如何开发的与 “Nightly Rust”](appendix-07-nightly-rust.md)

View File

@ -1,7 +1,7 @@
# 附录
> [appendix-00.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-00.md)
> [appendix-00.md](https://github.com/rust-lang/book/blob/master/src/appendix-00.md)
> <br>
> commit 4f2dc564851dc04b271a2260c834643dfd86c724
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
附录部分包含一些在你的Rust之旅中可能用到的参考资料。
附录部分包含一些在你的 Rust 之旅中可能用到的参考资料。

View File

@ -1,81 +1,109 @@
## 附录A - 关键字
## 附录 A 关键字
> [appendix-01-keywords.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-01-keywords.md)
> [appendix-01-keywords.md](https://raw.githubusercontent.com/rust-lang/book/master/src/appendix-01-keywords.md)
> <br>
> commit 32215c1d96c9046c0b553a05fa5ec3ede2e125c3
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
下面的列表中是Rust正在使用或者以后会用关键字。因此这些关键字不能被用作标识符例如
函数、变量、参数、结构体、模块、crate、常量、宏、静态值、属性、类型、trait 或生命周期
下面的列表包含 Rust 中正在使用或者以后会用到的关键字。因此,这些关键字不能被用作标识符(除了 [原始标识符][raw-identifiers]这包括函数、变量、参数、结构体字段、模块、crate、常量、宏、静态值、属性、类型、trait 或生命周期
的名字。
### 目前正在使用的关键字
* `as` - 强制类型转换或者对使用`use`和`extern crate`声明引入的项目重命名
如下关键字目前有对应其描述的功能。
* `as` - 强制类型转换,消除特定包含项的 trait 的歧义,或者对 `use``extern crate` 语句中的项重命名
* `break` - 立刻退出循环
* `const` - 定义常量或者 **不变原生指针** (*constant raw pointers*)
* `continue` - 跳出本次循环,进入下一次循环
* `crate` - 引入一个外部 **crate** 或一个代表 **crate** 的宏变量
* `else` - 创建 `if``if let` 控制流的分支
* `const` - 定义常量或不变裸指针constant raw pointer
* `continue` - 继续进入下一次循环迭代
* `crate` - 链接link一个外部 **crate** 或一个代表宏定义的 **crate** 的宏变量
* `dyn` - 动态分发 trait 对象
* `else` - 作为 `if``if let` 控制流结构的 fallback
* `enum` - 定义一个枚举
* `extern` - 引入一个外部 **crate** 、函数或变量
* `false` - 布尔值 `false`
* `extern` - 链接一个外部 **crate** 、函数或变量
* `false` - 布尔字面`false`
* `fn` - 定义一个函数或 **函数指针类型** (*function pointer type*)
* `for` - 遍历一个迭代器或实现一个 **trait**或者指定一个具体的生命周期
* `for` - 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期
* `if` - 基于条件表达式的结果分支
* `impl` - 实现一个方法或 **trait** 功能
* `in` - for循环语法的一部分
* `impl` - 实现自有或 trait 功能
* `in` - `for` 循环语法的一部分
* `let` - 绑定一个变量
* `loop` - 无条件循环
* `match` - 模式匹配
* `mod` - 定义一个模块
* `move` - 使闭包获取所有权
* `mut` - 表示一个可变绑定
* `pub` - 在结构体、`impl`块或模块中表示可以被外部使用
* `ref` - 绑定一个引用
* `move` - 使闭包获取其所捕获项的所有权
* `mut` - 表示引用、裸指针或模式绑定的可变性性
* `pub` - 表示结构体字段、`impl` 块或模块的公有可见性
* `ref` - 通过引用绑定
* `return` - 从函数中返回
* `Self` - 实现一个 **trait** 类型的类型别名
* `Self` - 实现 trait 类型的类型别名
* `self` - 表示方法本身或当前模块
* `static` - 表示全局变量或在整个程序执行期间保持其生命周期
* `struct` - 定义一个结构体
* `super` - 表示当前模块的父模块
* `trait` - 定义一个 **trait**
* `true` - 布尔值 `true`
* `type` - 定义一个类型别名或关联类型
* `unsafe` - 表示不安全的代码、函数、**traits**者方法实现
* `trait` - 定义一个 trait
* `true` - 布尔字面`true`
* `type` - 定义一个类型别名或关联类型
* `unsafe` - 表示不安全的代码、函数、trait 或实现
* `use` - 引入外部空间的符号
* `where` - 表示一个类型约束 [\[For example\]][ch13-01]
* `where` - 表示一个约束类型的从句
* `while` - 基于一个表达式的结果判断是否进行循环
[ch13-01]: ch13-01-closures.html#使用带有泛型和-fn-trait-的闭包
### 保留做将来使用的关键字
<!-- we should make sure the definitions for each keyword are consistently
phrased, so for example for enum we say "defining an enumeration" but for fn we
passively call it a "function definition" -- perhaps a good medium would be
"define an enumeration" and "define a function"? Can you go through and make
those consistent? I've attempted it for a few, but am wary of changing meaning.
Also, you may decide to go the passive definition route, which is fine by me,
as long as it's consistent-->
<!-- I've tried, I'm not sure how to be active for keywords that are nouns
though. Please let me know if any still seem inconsistent /Carol -->
### 未使用的保留字
这些关键字没有目前任何功能,但是它们是 Rust 未来会使用的保留字。
如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。
* `abstract`
* `alignof`
* `async`
* `become`
* `box`
* `do`
* `final`
* `macro`
* `offsetof`
* `override`
* `priv`
* `proc`
* `pure`
* `sizeof`
* `try`
* `typeof`
* `unsized`
* `virtual`
* `yield`
### 原始标识符
[raw-identifiers]: #raw-identifiers
原始标识符Raw identifiers允许你使用通常不能使用的关键字其带有 `r#` 前缀。
例如,`match` 是关键字。如果尝试编译这个函数:
```rust,ignore
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
```
会得到这个错误:
```text
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
```
可以通过原始标识符编写:
```rust
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert!(r#match("foo", "foobar"));
}
```
注意 `r#` 前缀需同时用于函数名和调用。
#### 动机
出于一些原因这个功能是实用的,不过其主要动机是解决跨版本问题。比如,`try` 在 2015 edition 中不是关键字,而在 2018 edition 则是。所以如果如果用 2015 edition 编写的库中带有 `try` 函数,在 2018 edition 中调用时就需要使用原始标识符。

View File

@ -1,21 +1,20 @@
## 附录B - 运算符与符号
> [appendix-02-operators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-02-operators.md)
## 附录 B运算符与符号
> [appendix-02-operators.md](https://github.com/rust-lang/book/blob/master/src/appendix-02-operators.md)
> <br />
> commit d50521fc08e51892cdf1edf5e35f3847a42f9432
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
[commit]: https://github.com/rust-lang/book/commit/d50521fc08e51892cdf1edf5e35f3847a42f9432
该附录包含了 Rust 语法的词汇表包括运算符以及其他的符号这些符号以其自身或者在路径、泛型、trait bounds、宏、属性、注释、元组以及大括号的上下文中出现。
该附录包含了 Rust 语法的词汇表包括运算符以及其他的符号这些符号单独出现或出现在路径、泛型、trait bounds、宏、属性、注释、元组以及大括号上下文中。
### 运算符
表B-1包含了 Rust 中的运算符、运算符如何出现在上下文中的示例、简短解释以及该运算符是否可重载。如果一个运算符是可重载的,则该运算符上用于重载的相关 trait 也会列出。
B-1 包含了 Rust 中的运算符、运算符如何出现在上下文中的示例、简短解释以及该运算符是否可重载。如果一个运算符是可重载的,则该运算符上用于重载的相关 trait 也会列出。
<span class="caption">表 B-1: 运算符</span>
<span class="caption">表 B-1: 运算符</span>
| 运算符 | 示例 | 解释 | 是否可重载 |
|----------|---------|-------------|---------------|
| `!` | `ident!(...)`, `ident!{...}`, `ident![...]` | 宏展 | |
| `!` | `ident!(...)`, `ident!{...}`, `ident![...]` | 宏展 | |
| `!` | `!expr` | 按位非或逻辑非 | `Not` |
| `!=` | `var != expr` | 不等比较 | `PartialEq` |
| `%` | `expr % expr` | 算术取模 | `Rem` |
@ -28,7 +27,7 @@
| `*` | `expr * expr` | 算术乘法 | `Mul` |
| `*=` | `var *= expr` | 算术乘法与赋值 | `MulAssign` |
| `*` | `*expr` | 解引用 | |
| `*` | `*const type`, `*mut type` | 原生指针 | |
| `*` | `*const type`, `*mut type` | 指针 | |
| `+` | `trait + trait`, `'a + trait` | 复合类型限制 | |
| `+` | `expr + expr` | 算术加法 | `Add` |
| `+=` | `var += expr` | 算术加法与赋值 | `AddAssign` |
@ -75,32 +74,34 @@
表 B-2 展示了以其自身出现以及出现在合法其他各个地方的符号。
<span class="caption">表 B-2独立语法</span>
<span class="caption">表 B-2独立语法</span>
| 符号 | 解释 |
|--------|-------------|
| `'ident` | 命名生命周期或循环标签 |
| `...u8`, `...i32`, `...f64`, `...usize`, 等 | 指定类型的数值常量 |
| `"..."` | 字符串常量 |
| `r"..."`, `r#"..."#`, `r##"..."##`, etc. | 原生字符串常量, 未处理的遗漏字符 |
| `b"..."` | 字节字符串; 构造一个 `[u8]` 类型而非字符串 |
| `br"..."`, `br#"..."#`, `br##"..."##`, 等 | 原生字节字符串常量,原生字节和字节结合的字符串 |
| `'...'` | 字符常量 |
| `b'...'` | ASCII码字节常量 |
| <code>\|...\| expr</code> | 结束 |
| `!` | 对一个离散函数来说最后总是空类型 |
| `_` | “忽略”模式绑定, 也用于整数常量的可读性 |
| `r"..."`, `r#"..."#`, `r##"..."##`, etc. | 原始字符串字面值, 未处理的转义字符 |
| `b"..."` | 字节字符串字面值; 构造一个 `[u8]` 类型而非字符串 |
| `br"..."`, `br#"..."#`, `br##"..."##`, 等 | 原始字节字符串字面值,原始和字节字符串字面值的结合 |
| `'...'` | 字符字面值 |
| `b'...'` | ASCII 码字节字面值 |
| <code>\|...\| expr</code> | 闭包 |
| `!` | 离散函数的总是为空的类型 |
| `_` | “忽略” 模式绑定;也用于增强整型字面值的可读性 |
<span class="caption">表 B-3 路径相关语法</span>
表 B-3 展示了出现在从模块结构到项的路径上下文中的符号
<span class="caption">表 B-3路径相关语法</span>
| 符号 | 解释 |
|--------|-------------|
| `ident::ident` | 命名空间路径 |
| `::path` | 与crate根相关的路径如一个明确的绝对路径) |
| `self::path` | 当前模块相关路径(如一个明确相关路径)|
| `super::path` | 父模块相关路径 |
| `type::ident`, `<type as trait>::ident` | 关常量、函数以及类型 |
| `<type>::...` | 不可以被直接命名的关项类型(如 `<&T>::...``<[T]>::...` 等) |
| `::path` | 与 crate 根相对的路径(如一个显式绝对路径) |
| `self::path` | 与当前模块相对的路径(如一个显式相对路径)|
| `super::path` | 与父模块相对的路径 |
| `type::ident`, `<type as trait>::ident` | 关常量、函数以及类型 |
| `<type>::...` | 不可以被直接命名的关项类型(如 `<&T>::...``<[T]>::...` 等) |
| `trait::method(...)` | 通过命名定义的 trait 来消除方法调用的二义性 |
| `type::method(...)` | 通过命名定义的类型来消除方法调用的二义性 |
| `<type as trait>::method(...)` | 通过命名 trait 和类型来消除方法调用的二义性 |
@ -108,12 +109,12 @@
表 B-4 展示了出现在泛型类型参数上下文中的符号。
<span class="caption">表 B-4泛型</span>
<span class="caption">表 B-4泛型</span>
| 符号 | 解释 |
|--------|-------------|
| `path<...>` | 为一个类型中的泛型指定具体参数(如 `Vec<u8>` |
| `path::<...>`, `method::<...>` | 为一个泛型、函数或表达式中的方法指定具体参数,通常指 [turbofish][turbofish] (如 `"42".parse::<i32>()`|
| `path::<...>`, `method::<...>` | 为一个泛型、函数或表达式中的方法指定具体参数,通常指 turbofish`"42".parse::<i32>()`|
| `fn ident<...> ...` | 泛型函数定义 |
| `struct ident<...> ...` | 泛型结构体定义 |
| `enum ident<...> ...` | 泛型枚举定义 |
@ -121,11 +122,9 @@
| `for<...> type` | 高级生命周期限制 |
| `type<ident=type>` | 泛型,其一个或多个相关类型必须被指定为特定类型(如 `Iterator<Item=T>`|
[turbofish]: https://matematikaadit.github.io/posts/rust-turbofish.html
表 B-5 展示了出现在使用 trait bounds 约束泛型参数上下文中的符号。
<span class="caption">表 B-5: Trait Bound 约束</span>
<span class="caption">表 B-5: Trait Bound 约束</span>
| 符号 | 解释 |
|--------|-------------|
@ -138,7 +137,7 @@
表 B-6 展示了在调用或定义宏以及在其上指定属性时的上下文中出现的符号。
<span class="caption">表 B-6: 宏与属性</span>
<span class="caption">表 B-6: 宏与属性</span>
| 符号 | 解释 |
|--------|-------------|
@ -150,7 +149,7 @@
表 B-7 展示了写注释的符号。
<span class="caption">表 B-7: 注释</span>
<span class="caption">表 B-7: 注释</span>
| 符号 | 注释 |
|--------|-------------|
@ -163,28 +162,32 @@
表 B-8 展示了出现在使用元组时上下文中的符号。
<span class="caption">表 B-8: 元组</span>
| 符号 | 解释 |
|--------|-------------|
| `()` | 空元祖(亦称单元), 用于常量或类型中 |
| `()` | 空元组(亦称单元),即是字面值也是类型 |
| `(expr)` | 括号表达式 |
| `(expr,)` | 单一元素元组表达式 |
| `(type,)` | 单一元素元组类型 |
| `(expr, ...)` | 元组表达式 |
| `(type, ...)` | 元组类型 |
| `expr(expr, ...)` | 函数调用表达式; 也用于初始化元组结构体 `struct` 以及元组枚举 `enum` 变体 |
| `expr(expr, ...)` | 函数调用表达式;也用于初始化元组结构体 `struct` 以及元组枚举 `enum` 变体 |
| `ident!(...)`, `ident!{...}`, `ident![...]` | 宏调用 |
| `expr.0`, `expr.1`, etc. | 元组索引 |
表 B-9 使用大括号的符号。
表 B-9 展示了使用大括号的上下文。
<span class="caption">表 B-9: 大括号</span>
| 符号 | 解释 |
|---------|-------------|
| `{...}` | 块表达式 |
| `Type {...}` | `struct` |
| `Type {...}` | `struct` 字面值 |
表 B-10 展示了使用方括号的符号
表 B-10 展示了使用方括号的上下文
<span class="caption">表 B-10: 方括号</span>
<span class="caption">表 B-10: 方括号</span>
| 符号 | 解释 |
|---------|-------------|
@ -192,4 +195,4 @@
| `[expr; len]` | 复制了 `len``expr`的数组 |
| `[type; len]` | 包含 `len``type` 类型的数组|
| `expr[expr]` | 集合索引。 重载(`Index`, `IndexMut` |
| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 集合索引,使用 `Range``RangeFrom``RangeTo` 或 `RangeFull` 作为索引来代替集合切片 |
| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 集合索引,使用 `Range``RangeFrom``RangeTo` 或 `RangeFull` 作为索引来代替集合 slice |

View File

@ -1,15 +1,12 @@
## 附录C - 可派生的 trait
## 附录 C可派生的 trait
> [appendix-03-derivable-traits.md][appendix-03]
> [appendix-03-derivable-traits.md](https://github.com/rust-lang/book/blob/master/src/appendix-03-derivable-traits.md)
> <br />
> commit 32215c1d96c9046c0b553a05fa5ec3ede2e125c3
> commit a86c1d315789b3ca13b20d50ad5005c62bdd9e37
[appendix-03]: https://github.com/rust-lang/book/blob/master/second-edition/src/appendix-03-derivable-traits.md
[commit]: https://github.com/rust-lang/book/commit/32215c1d96c9046c0b553a05fa5ec3ede2e125c3
在本书的各个部分中,我们讨论了可应用于结构体和枚举定义的 `derive` 属性。`derive` 属性会在使用 `derive` 语法标记的类型上生成对应 trait 的默认实现的代码。
在本书的各个部分中,我们讨论了可应用于结构体和枚举的 `derive` 属性。`derive` 属性生成的代码在使用 `derive` 语法注释的类型之上实现了带有默认实现的 trait 。
在该附录中,我们提供标准库中所有可以使用 `derive` 的 trait 的参考。每部分都包含:
在本附录中提供了标准库中所有所有可以使用 `derive` 的 trait 的参考。这些部分涉及到:
* 该 trait 将会派生什么样的操作符和方法
* 由 `derive` 提供什么样的 trait 实现
@ -17,31 +14,31 @@
* 是否允许实现该 trait 的条件
* 需要 trait 操作的例子
`derive` 属性提供的行为相比如果你需要与之不同的行为,请查阅标准库文档以获取每个 trait 的详情,来手动实现它们。
如果你希望不同于 `derive` 属性所提供的行为,请查阅 [标准库文档](https://doc.rust-lang.org/std/index.html) 中每个 trait 的细节以了解如何手动实现它们。
在类型上无法使用 `derive` 实现标准库的其余 trait。这些 trait 没有合理的默认行为, 因此,你可以以一种尝试完成的合理方式实现它们。
标准库中定义的其它 trait 不能通过 `derive` 在类型上实现。这些 trait 不存在有意义的默认行为,所以由你负责以合理的方式实现它们。
一个无法被派生的 trait 的例子是为终用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为终用户显示一个类型。终用户应该看到类型的什么部分?他们会找出相关部分吗?对他们来说最相的数据格式是什么样的Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。
一个无法被派生的 trait 的例子是为终用户处理格式化的 `Display` 。你应该时常考虑使用合适的方法来为终用户显示一个类型。终用户应该看到类型的什么部分他们会找出相关部分吗对他们来说最相关的数据格式是什么样的Rust 编译器没有这样的洞察力,因此无法为你提供合适的默认行为。
本附录所提供的可派生 trait 列表并不全面:库可以为它们自己的 trait 实现 `derive` , 让可以使用 `derive` 的 trait 列表真诚的开放。实现 `derive` 涉及使用程序化宏这在附录D中有介绍。
本附录所提供的可派生 trait 列表并不全面:库可以为其自己的 trait 实现 `derive`,可以使用 `derive` 的 trait 列表事实上是无限的。实现 `derive` 涉及到过程宏的应用,这在第十九章的 “宏” 有介绍。
## 编程人员输出的 `Debug`
### 用于程序员输出的 `Debug`
`Debug` trait 在格式化字符串中使调试格式化,你可以在 `{}` 占位符里面加上 `:?` 显示它
`Debug` trait 用于开启格式化字符串中的调试格式,其通过在 `{}` 占位符中增加 `:?` 表明
`Debug` trait 允许你以调试目的来打印一个类型的实例,因此,使用类型的你以及其他的编程人员可以让程序在执行时在指定点上显示一个实例。
`Debug` trait 允许以调试目的来打印一个类型的实例,所以使用该类型的程序员可以在程序执行的特定时间点观察其实例。
例如,在使用 `assert_eq!` 宏时Debug` trait 是必须的。如果等式断言失败,这个宏就把给定实例的值作为参数打印出来,因此,编程人员可以看到两个实例为什么不相等。
例如,在使用 `assert_eq!` 宏时,`Debug` trait 是必须的。如果等式断言失败,这个宏就把给定实例的值作为参数打印出来,如此程序员可以看到两个实例为什么不相等。
## 等值比较的 `PartialEq``Eq`
`PartialEq` trait 可以比较一个类型的实例以检查是否相等,并且可以使用 `==``!=` 操作符
`PartialEq` trait 可以比较一个类型的实例以检查是否相等,并开启了 `==``!=` 运算符的功能
派生的 `PartialEq` 实现了 `eq` 方法。当 `PartialEq` 在结构体上派生时,只有*所有*的字段都相等时两个实例才相等,同时只要有字段不相等则两个实例就不相等。当在枚举上派生时,每一个变体(variant)都和它自身相等,且和其他变体都不相等。
派生的 `PartialEq` 实现了 `eq` 方法。当 `PartialEq` 在结构体上派生时,只有*所有* 的字段都相等时两个实例才相等,同时只要有任何字段不相等则两个实例就不相等。当在枚举上派生时,每一个成员都和其自身相等,且和其他成员都不相等。
例如,当使用 `assert_eq!` 宏时,需要比较比较一个类型的两个实例是否相等,则 `PartialEq` trait 是必须的。
`Eq` trait 没有方法。其目的是为每一个注解类型的值作标志,其值等于其自身。 `Eq` trait 只能应用于那些实现了 `PartialEq` 的类型,但并非所有实现了 `PartialEq` 的类型可以实现 `Eq`。浮点类型就是一个例子:浮点数状态的实现,两个非数字(`NaN`not-a-number值是互不相等的。
`Eq` trait 没有方法。其作用是表明每一个被标记类型的值等于其自身。`Eq` trait 只能应用于那些实现了 `PartialEq` 的类型,但并非所有实现了 `PartialEq` 的类型可以实现 `Eq`。浮点类型就是一个例子:浮点数的实现表明两个非数字(`NaN`not-a-number值是互不相等的。
例如,对于一个 `HashMap<K, V>` 中的 key 来说, `Eq` 是必须的,这样 `HashMap<K, V>` 就可以知道两个 key 是否一样了。
@ -49,19 +46,19 @@
`PartialOrd` trait 可以基于排序的目的而比较一个类型的实例。实现了 `PartialOrd` 的类型可以使用 `<``>`、`<=` 和 `>=` 操作符。但只能在同时实现了 `PartialEq` 的类型上使用 `PartialOrd`
派生 `PartialOrd` 实现了 `partial_cmp` 方法,其返回一个 `Option<Ordering>` ,但当给定值无法产生顺序时将返回 `None`。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值`NaN`not-a-number。当在浮点数上调用 `partial_cmp` 时,`NaN` 的浮点数将返回 `None`
派生 `PartialOrd` 实现了 `partial_cmp` 方法,其返回一个 `Option<Ordering>` ,但当给定值无法产生顺序时将返回 `None`。尽管大多数类型的值都可以比较,但一个无法产生顺序的例子是:浮点类型的非数字值。当在浮点数上调用 `partial_cmp` 时,`NaN` 的浮点数将返回 `None`
当在结构体上派生时,`PartialOrd` 以在结构体定义中字段出现的顺序比较每个字段的值来比较两个实例。当在枚举上派生时,认为在枚举定义中声明较早的枚举变体小于其后的变体。
例如,对于来自于 `rand` carte 中的 `gen_range` 方法来说,当在一个大值和小值指定的范围内生成一个随机值时,`PartialOrd` trait 是必须的。
`Ord` trait 也让你明白在一个带注解类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option<Ordering>`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd``Eq` `Eq` 依赖 `PartialEq` )的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。
`Ord` trait 也让你明白在一个带注解类型上的任意两个值存在有效顺序。`Ord` trait 实现了 `cmp` 方法,它返回一个 `Ordering` 而不是 `Option<Ordering>`,因为总存在一个合法的顺序。只可以在实现了 `PartialOrd``Eq``Eq` 依赖 `PartialEq`)的类型上使用 `Ord` trait 。当在结构体或枚举上派生时, `cmp` 和以 `PartialOrd` 派生实现的 `partial_cmp` 表现一致。
例如,当在 `BTreeSet<T>` (一种基于有序值存储数据的数据结构)上存值时,`Ord` 是必须的。
例如,当在 `BTreeSet<T>`(一种基于有序值存储数据的数据结构)上存值时,`Ord` 是必须的。
## 复制值的 `Clone``Copy`
`Clone` trait 可以明确地创建一个值的深拷贝( deep copy ),复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第四章“变量和数据的交互方式:移动”以获取有关 `Clone` 的更多信息。
`Clone` trait 可以明确地创建一个值的深拷贝deep copy复制过程可能包含任意代码的执行以及堆上数据的复制。查阅第四章 “变量和数据的交互方式:移动” 以获取有关 `Clone` 的更多信息。
派生 `Clone` 实现了 `clone` 方法,其为整个的类型实现时,在类型的每一部分上调用了 `clone` 方法。这意味着类型中所有字段或值也必须实现 `Clone` 来派生 `Clone`

View File

@ -0,0 +1,5 @@
## 附录 E实用开发工具
> [appendix-04-useful-development-tools.md](https://github.com/rust-lang/book/blob/master/src/appendix-04-useful-development-tools.md)
> <br />
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f

View File

@ -0,0 +1,5 @@
## 附录 E版本
> [appendix-05-editions.md](https://github.com/rust-lang/book/blob/master/src/appendix-05-editions.md)
> <br />
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f

View File

@ -0,0 +1,25 @@
## 附录 F本书译本
> [appendix-06-translation.md](https://github.com/rust-lang/book/blob/master/src/appendix-06-translation.md)
> <br />
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
一些非英语语言的资源。多数仍在翻译中;查阅 [翻译标签][label] 来帮助我们或使我们知道新的翻译!
[label]: https://github.com/rust-lang/book/issues?q=is%3Aopen+is%3Aissue+label%3ATranslations
- [Português](https://github.com/rust-br/rust-book-pt-br) (BR)
- [Português](https://github.com/nunojesus/rust-book-pt-pt) (PT)
- [Tiếng việt](https://github.com/hngnaig/rust-lang-book/tree/vi-VN)
- [简体中文](http://www.broadview.com.cn/article/144), [另一个版本(译者注:已基本翻译完毕)](https://github.com/KaiserY/trpl-zh-cn)
- [Українська](https://github.com/pavloslav/rust-book-uk-ua)
- [Español](https://github.com/thecodix/book)
- [Italiano](https://github.com/AgeOfWar/rust-book-it)
- [Русский](https://github.com/iDeBugger/rust-book-ru)
- [한국어](https://github.com/rinthel/rust-lang-book-ko)
- [日本語](https://github.com/hazama-yuinyan/book)
- [Français](https://github.com/quadrifoglio/rust-book-fr)
- [Polski](https://github.com/paytchoo/book-pl)
- [עברית](https://github.com/idanmel/rust-book-heb)
- [Cebuano](https://github.com/agentzero1/book)
- [Tagalog](https://github.com/josephace135/book)

View File

@ -1 +1,5 @@
## G - Rust 是如何开发的与 “Nightly Rust”
## 附录 GRust 是如何开发的与 “Nightly Rust”
> [appendix-07-nightly-rust.md](https://github.com/rust-lang/book/blob/master/src/appendix-07-nightly-rust.md)
> <br />
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f

View File

@ -1,20 +1,18 @@
## 高级类型
> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-04-advanced-types.md)
> [ch19-04-advanced-types.md](https://github.com/rust-lang/book/blob/master/src/ch19-04-advanced-types.md)
> <br>
> commit 9d5b9a573daf5fa0c98b3a3005badcea4a0a5211
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
Rust 的类型系统有一些我们曾经提到但没有讨论过的功能。首先我们从一个关于为什么 newtype 与类型一样有用的更宽泛的讨论开始。接着会转向类型别名type aliases一个类似于 newtype 但有着稍微不同的语义的功能。我们还会讨论 `!` 类型和动态大小类型。
### 为了类型安全和抽象而使用 newtype 模式
> 这一部分假设你已经阅读了之前的 “newtype 模式用于在外部类型上实现外部 trait” 部分。
> 这一部分假设你已经阅读了 “高级 trait” 部分的 newtype 模式相关内容。
### 为了类型安全和抽象而使用 newtype 模式
newtype 模式可以用于一些其他我们还未讨论的功能,包括静态的确保某值不被混淆,和用来表示一个值的单元。实际上示例 19-23 中已经有一个这样的例子:`Millimeters` 和 `Meters` 结构体都在 newtype 中封装了 `u32` 值。如果编写了一个有 `Millimeters` 类型参数的函数,不小心使用 `Meters` 或普通的 `u32` 值来调用该函数的程序是不能编译的。
另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的 API以便限制其功能。
另一个 newtype 模式的应用在于抽象掉一些类型的实现细节:例如,封装类型可以暴露出与直接使用其内部私有类型时所不同的公有 API以便限制其功能。
newtype 也可以隐藏其内部的泛型类型。例如,可以提供一个封装了 `HashMap<i32, String>``People` 类型,用来储存人名以及相应的 ID。使用 `People` 的代码只需与提供的公有 API 交互即可,比如向 `People` 集合增加名字字符串的方法,这样这些代码就无需知道在内部我们将一个 `i32` ID 赋予了这个名字了。newtype 模式是一种实现第十七章 “封装隐藏了实现细节” 部分所讨论的隐藏实现细节的封装的轻量级方法。
@ -42,19 +40,19 @@ println!("x + y = {}", x + y);
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
```rust,ignore
Box<Fn() + Send + 'static>
Box<dyn Fn() + Send + 'static>
```
在函数签名或类型注解中每次都书写这个类型将是枯燥且易于出错的。想象一下如示例 19-32 这样全是如此代码的项目:
```rust
let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi"));
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<Fn() + Send + 'static>) {
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<Fn() + Send + 'static> {
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
# Box::new(|| ())
}
@ -65,7 +63,7 @@ fn returns_long_type() -> Box<Fn() + Send + 'static> {
类型别名通过减少项目中重复代码的数量来使其更加易于控制。这里我们为这个冗长的类型引入了一个叫做 `Thunk` 的别名,这样就可以如示例 19-33 所示将所有使用这个类型的地方替换为更短的 `Thunk`
```rust
type Thunk = Box<Fn() + Send + 'static>;
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
@ -104,7 +102,7 @@ pub trait Write {
type Result<T> = Result<T, std::io::Error>;
```
因为这位于 `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,ignore
pub trait Write {
@ -118,7 +116,7 @@ pub trait Write {
类型别名在两个方面有帮助:易于编写 **并** 在整个 `std::io` 中提供了一致的接口。因为这是一个别名,它只是另一个 `Result<T, E>`,这意味着可以在其上使用 `Result<T, E>` 的任何方法,以及像 `?` 这样的特殊语法。
### 从不返回的 `!`never type
### 从不返回的 never type
Rust 有一个叫做 `!` 的特殊类型。在类型理论术语中,它被称为 *empty type*,因为它没有值。我们更倾向于称之为 *never type*。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如:
@ -128,10 +126,9 @@ fn bar() -> ! {
}
```
这读 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数***diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回。
这读 “函数 `bar` 从不返回”,而从不返回的函数被称为 **发散函数***diverging functions*)。不能创建 `!` 类型的值,所以 `bar` 也不可能返回
不过一个不能创建值的类型有什么用呢?如果你回想一下第二章,曾经有一些看起来像这样的代码,如示例 19-34 所重现的:
不过一个不能创建值的类型有什么用呢?如果你回想一下示例 2-5 中的代码,曾经有一些看起来像这样的代码,如示例 19-34 所重现的:
```rust
# let guess = "3";
@ -148,27 +145,19 @@ let guess: u32 = match guess.trim().parse() {
当时我们忽略了代码中的一些细节。在第六章 “`match` 控制流运算符” 部分,我们学习了 `match` 的分支必须返回相同的类型。如下代码不能工作:
```rust,ignore
let guess = match guess.trim().parse() {
```rust,ignore,does_not_compile
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
}
```
这里的 `guess` 必须既是整型也是字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么示例 19-34 中会允许一个分支返回 `u32` 而另一个分支却以 `continue` 结束呢?
这里的 `guess` 必须既是整型 **也是** 字符串,而 Rust 要求 `guess` 只能是一个类型。那么 `continue` 返回了什么呢?为什么示例 19-34 中会允许一个分支返回 `u32` 而另一个分支却以 `continue` 结束呢?
正如你可能猜到的,`continue` 的值是 `!`。也就是说,当 Rust 要计算 `guess` 的类型时,它查看这两个分支。前者是 `u32` 值,而后者是 `!` 值。因为 `!` 并没有一个值Rust 决定 `guess` 的类型是 `u32`
描述 `!` 的行为的正式方式是 never type 可以强转为任何其他类型。允许 `match` 的分支以 `continue` 结束是因为 `continue` 并不真正返回一个值;相反它把控制权交回上层循环,所以在 `Err` 的情况,事实上并未对 `guess` 赋值。
<!-- I'm not sure I'm following what would then occur in the event of an error,
literally nothing? -->
<!-- The block returns control to the enclosing loop; I'm not sure how to
clarify this other than what we already have here, do you have any suggestions?
I wouldn't say it's "literally nothing" because it does do something, it
returns control to the loop and the next iteration of the loop happens...
/Carol -->
never type 的另一个用途是 `panic!`。还记得 `Option<T>` 上的 `unwrap` 函数吗?它产生一个值或 panic。这里是它的定义
```rust,ignore
@ -182,7 +171,7 @@ impl<T> Option<T> {
}
```
这里与示例 19-34 中的 `match` 发生了相同的情况:我们知道 `val``T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效。
这里与示例 19-34 中的 `match` 发生了相同的情况:Rust 知道 `val``T` 类型,`panic!` 是 `!` 类型,所以整个 `match` 表达式的结果是 `T` 类型。这能工作是因为 `panic!` 并不产生一个值;它会终止程序。对于 `None` 的情况,`unwrap` 并不返回一个值,所以这些代码是有效。
最后一个有着 `!` 类型的表达式是 `loop`
@ -202,16 +191,11 @@ loop {
让我们深入研究一个贯穿本书都在使用的动态大小类型的细节:`str`。没错,不是 `&str`,而是 `str` 本身。`str` 是一个 DST直到运行时我们都不知道字符串有多长。因为直到运行时都不能知道大其小也就意味着不能创建 `str` 类型的变量,也不能获取 `str` 类型的参数。考虑一下这些代码,他们不能工作:
```rust,ignore
```rust,ignore,does_not_compile
let s1: str = "Hello there!";
let s2: str = "How's it going?";
```
<!-- Why do they need to have the same memory layout? Perhaps I'm not
understanding fully what is meant by the memory layout, is it worth explaining
that a little in this section? -->
<!-- I've reworded /Carol -->
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 `str` 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
那么该怎么办呢?你已经知道了这种问题的答案:`s1` 和 `s2` 的类型是 `&str` 而不是 `str`。如果你回想第四章 “字符串 slice” 部分slice 数据结储存了开始位置和 slice 的长度。
@ -220,15 +204,6 @@ Rust 需要知道应该为特定类型的值分配多少内存,同时所有同
可以将 `str` 与所有类型的指针结合:比如 `Box<str>``Rc<str>`。事实上之前我们已经见过了不过是另一个动态大小类型trait。每一个 trait 都是一个可以通过 trait 名称来引用的动态大小类型。在第十七章 “为使用不同类型的值而设计的 trait 对象” 部分,我们提到了为了将 trait 用于 trait 对象,必须将他们放入指针之后,比如 `&Trait``Box<Trait>``Rc<Trait>` 也可以。trait 之所以是动态大小类型的是因为只有这样才能使用它。
#### `Sized` trait
<!-- If we end up keeping the section on object safety in ch 17, we should add
a back reference here. /Carol -->
<!-- I think we dropped that one, right? -->
<!-- We cut a large portion of it, including the part about `Sized`, so I
didn't add a back reference. /Carol -->
为了处理 DSTRust 有一个特定的 trait 来决定一个类型的大小是否在编译时可知:这就是 `Sized` trait。这个 trait 自动为编译器在编译时就知道大小的类型实现。另外Rust 隐式的为每一个泛型函数增加了 `Sized` bound。也就是说对于如下泛型函数定义
```rust,ignore

View File

@ -1,14 +1,14 @@
## 高级函数与闭包
> [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch19-05-advanced-functions-and-closures.md)
> [ch19-05-advanced-functions-and-closures.md](https://github.com/rust-lang/book/blob/master/src/ch19-05-advanced-functions-and-closures.md)
> <br>
> commit 509cb42ece610bdac8eaad26d57fb604dc078623
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
最后我们将探索一些有关函数和闭包的高级功能:函数指针以及返回值闭包。
### 函数指针
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为**函数指针***function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-34 所示:
我们讨论过了如何向函数传递闭包;也可以向函数传递常规函数!这在我们希望传递已经定义的函数而不是重新定义闭包作为参数是很有用。通过函数指针允许我们使用函数作为另一个函数的参数。函数的类型是 `fn` (使用小写的 “f” )以免与 `Fn` 闭包 trait 相混淆。`fn` 被称为 **函数指针***function pointer*)。指定参数为函数指针的语法类似于闭包,如示例 19-35 所示:
<span class="filename">文件名: src/main.rs</span>
@ -28,7 +28,7 @@ fn main() {
}
```
<span class="caption">示例 19-35: 使用 `fn` 类型接受函数指针作为参数</span>
<span class="caption">示例 19-34: 使用 `fn` 类型接受函数指针作为参数</span>
这会打印出 `The answer is: 12`。`do_twice` 中的 `f` 被指定为一个接受一个 `i32` 参数并返回 `i32``fn`。接着就可以在 `do_twice` 函数体中调用 `f`。在 `main` 中,可以将函数名 `add_one` 作为第一个参数传递给 `do_twice`
@ -36,7 +36,7 @@ fn main() {
函数指针实现了所有三个闭包 trait`Fn`、`FnMut` 和 `FnOnce`),所以总是可以在调用期望闭包的函数时传递函数指针作为参数。倾向于编写使用泛型和闭包 trait 的函数,这样它就能接受函数或闭包作为参数。
一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时C 语言的函数可以接受函数作为参数,但没有闭包。
一个只期望接受 `fn` 而不接受闭包的情况的例子是与不存在闭包的外部代码交互时C 语言的函数可以接受函数作为参数,但 C 语言没有闭包。
作为一个既可以使用内联定义的闭包又可以使用命名函数的例子,让我们看看一个 `map` 的应用。使用 `map` 函数将一个数字 vector 转换为一个字符串 vector就可以使用闭包比如这样
@ -60,6 +60,20 @@ let list_of_strings: Vec<String> = list_of_numbers
注意这里必须使用 “高级 trait” 部分讲到的完全限定语法,因为存在多个叫做 `to_string` 的函数;这里使用了定义于 `ToString` trait 的 `to_string` 函数,标准库为所有实现了 `Display` 的类型实现了这个 trait。
另一个实用的模式暴露了元组结构体和元组结构体枚举成员的实现细节。这些项使用 `()` 作为初始化语法,这看起来就像函数调用,同时它们确实被实现为返回由参数构造的实例的函数。它们也被称为实现了闭包 trait 的函数指针,并可以采用类似如下的方式调用:
```rust
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> =
(0u32..20)
.map(Status::Value)
.collect();
```
一些人倾向于函数风格,一些人喜欢闭包。这两种形式最终都会产生同样的代码,所以请使用对你来说更明白的形式吧。
### 返回闭包
@ -68,7 +82,7 @@ let list_of_strings: Vec<String> = list_of_numbers
这段代码尝试直接返回闭包,它并不能编译:
```rust,ignore
```rust,ignore,does_not_compile
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
@ -93,15 +107,11 @@ std::marker::Sized` is not satisfied
错误又一次指向了 `Sized` traitRust 并不知道需要多少空间来储存闭包。不过我们在上一部分见过这种情况的解决办法:可以使用 trait 对象:
```rust
fn returns_closure() -> Box<Fn(i32) -> i32> {
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
```
这段代码正好可以编译。关于 trait 对象的更多内容,请回顾第十七章的 “为使用不同类型的值而设计的 trait 对象” 部分。
## 总结
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
接下来,我们将再开始一个项目,将本书所学的所有内容付与实践!
接下来让我们学习宏!

362
src/ch19-06-macros.md Normal file
View File

@ -0,0 +1,362 @@
## 宏
> [ch19-06-macros.md](https://github.com/rust-lang/book/blob/master/src/ch19-06-macros.md)
> <br>
> commit 6babe749f8d97131b97c3cb9e7ca76e6115b90c1
我们已经在本书中使用过像 `println!` 这样的宏了,不过还没完全探索什么是宏以及它是如何工作的。**宏***Macro*)指的是 Rust 中一系列的功能:
* **声明***Declarative*)宏,使用 `macro_rules!`
* **过程***Procedural*),其有三种类型:
* 自定义 `#[derive]`
* 类属性Attribute
* 类函数宏
我们会依次讨论每一种宏,不过首要的是,为什么已经有了函数还需要宏呢?
### 宏和函数的区别
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 **元编程***metaprogramming*)。在附录 C 中会探讨 `derive` 属性,其生成各种 trait 的实现。我们也在本书中使用过 `println!` 宏和 `vec!` 宏。所有的这些宏 **展开** 来以生成比你手写更多的代码。
元编程对于减少大量编写和维护的代码是非常有用的,它也扮演了函数的角色。但宏有一些函数所没有的附加能力。
一个函数标签必须声明函数参数个数和类型。相比之下,宏只接受一个可变参数:用一个参数调用 `println!("hello")` 或用两个参数调用 `println!("hello {}", name)` 。而且,宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。因为函数是在运行时被调用,同时 trait 需要在运行时实现,所以函数无法像宏这样。
实现一个宏而不是函数的消极面是宏定义要比函数定义更复杂,因为你正在编写生成 Rust 代码的 Rust 代码。由于这样的间接性,宏定义通常要比函数定义更难阅读、理解以及维护。
宏和函数的最后一个重要的区别是:在调用宏 **之前** 必须定义并将其引入作用域,而函数则可以在任何地方定义和调用。
### 使用 `macro_rules!` 的声明宏用于通用元编程
Rust 最常用的宏形式是 **声明宏***declarative macros*)。它们有时也被称为 “macros by example”、“`macro_rules!` 宏” 或者就是 “macros”。其核心概念是声明宏允许我们编写一些类似 Rust `match` 表达式的代码。正如在第六章讨论的那样,`match` 表达式是控制结构,其接收一个表达式,与表达式的结果进行模式匹配,然后根据模式匹配执行相关代码。宏也将一个值和包含相关代码的模式进行比较;此种情况下,该值是传递给宏的 Rust 源代码字面值,模式用于和传递给宏的源代码进行比较,同时每个模式的相关代码则用于替换传递给宏的代码。所有这一切都发生于编译时。
可以使用 `macro_rules!` 来定义宏。让我们通过查看 `vec!` 宏定义来探索如何使用 `macro_rules!` 结构。第八章讲述了如何使用 `vec!` 宏来生成一个给定值的 vector。例如下面的宏用三个整数创建一个 vector `
```rust
let v: Vec<u32> = vec![1, 2, 3];
```
也可以使用 `vec!` 宏来构造两个整数的 vector 或五个字符串 slice 的 vector 。但却无法使用函数做相同的事情,因为我们无法预先知道参数值的数量和类型。
在示例 19-36 中展示了一个 `vec!` 稍微简化的定义。
<span class="filename">文件名: src/lib.rs</span>
```rust
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
```
<span class="caption">示例 19-36: 一个 `vec!` 宏定义的简化版本</span>
> 注意:标准库中实际定义的 `vec!` 包括预分配适当量的内存的代码。这部分为代码优化,为了让示例简化,此处并没有包含在内。
无论何时导入定义了宏的包,`#[macro_export]` 注解说明宏应该是可用的。 如果没有该注解,这个宏不能被引入作用域。
接着使用 `macro_rules!` 和宏名称开始宏定义,且所定义的宏并 *不带* 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 `vec`
`vec!` 宏的结构和 `match` 表达式的结构类似。此处有一个单边模式 `( $( $x:expr ),* )` ,后跟 `=>` 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这只是这个宏中的模式,且只有一个有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
宏定义中有效模式语法和在第十八章提及的模式语法是不同的,因为宏模式所匹配的是 Rust 代码结构而不是值。回过头来检查下示例 D-1 中模式片段什么意思。对于全部的宏模式语法,请查阅[参考]。
[参考]: https://doc.rust-lang.org/reference/macros.html
首先,一对括号包含了全部模式。接下来是后跟一对括号的美元符号( `$` ),其通过替代代码捕获了符合括号内模式的值。`$()` 内则是 `$x:expr` ,其匹配 Rust 的任意表达式或给定 `$x` 名字的表达式。
`$()` 之后的逗号说明一个逗号分隔符可以有选择的出现代码之后,这段代码与在 `$()` 中所捕获的代码相匹配。紧随逗号之后的 `*` 说明该模式匹配零个或多个 `*` 之前的任何模式。
当以 `vec![1, 2, 3];` 调用宏时,`$x` 模式与三个表达式 `1`、`2` 和 `3` 进行了三次匹配。
现在让我们来看看这个出现在与此单边模式相关的代码块中的模式:在 `$()*` 部分中所生成的 `temp_vec.push()` 为在匹配到模式中的 `$()` 每一部分而生成。`$x` 由每个与之相匹配的表达式所替换。当以 `vec![1, 2, 3];` 调用该宏时,替换该宏调用所生成的代码会是下面这样:
```rust,ignore
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
```
我们已经定义了一个宏,其可以接收任意数量和类型的参数,同时可以生成能够创建包含指定元素的 vector 的代码。
`macro_rules!` 中有一些奇怪的地方。在将来,会有第二种采用 `macro` 关键字的声明宏,其工作方式类似但修复了这些极端情况。在此之后,`macro_rules!` 实际上就过时deprecated了。在此基础之上同时鉴于大多数 Rust 程序员 **使用** 宏而非 **编写** 宏的事实,此处不再深入探讨 `macro_rules!`。请查阅在线文档或其他资源,如 [“The Little Book of Rust Macros”][tlborm] 来更多地了解如何写宏。
[tlborm]: https://danielkeep.github.io/tlborm/book/index.html
### 用于从属性生成代码的过程宏
第二种形式的宏被称为 **过程宏***procedural macros*),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏,不过它们的工作方式都类似。其一,其定义必须位于一种特殊类型的属于它们自己的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。
其二,使用这些宏需采用类似示例 19-37 所示的代码形式,其中 `some_attribute` 是一个使用特定宏的占位符。
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
```
<span class="caption">示例 19-37: 一个使用过程宏的例子</span>
过程宏包含一个函数,这也是其得名的原因:“过程” 是 “函数” 的同义词。那么为何不叫 “函数宏” 呢?好吧,有一个过程宏是 “类函数” 的,叫成函数会产生混乱。无论如何,定义过程宏的函数接受一个 `TokenStream` 作为输入并产生一个 `TokenStream` 作为输出。这也就是宏的核心:宏所处理的源代码组成了输入 `TokenStream`,同时宏生成的代码是输出 `TokenStream`。最后,函数上有一个属性;这个属性表明过程宏的类型。在同一 crate 中可以有多种的过程宏。
考虑到这些宏是如此类似,我们会从自定义派生宏开始。接着会解释与其他形式宏的微小区别。
### 如何编写自定义 `derive`
让我们创建一个 `hello_macro` crate其包含名为 `HelloMacro` 的 trait 和关联函数 `hello_macro`。不同于让 crate 的用户为其每一个类型实现 `HelloMacro` trait我们将会提供一个过程式宏以便用户可以使用 `#[derive(HelloMacro)]` 注解他们的类型来得到 `hello_macro` 函数的默认实现。该默认实现会打印 `Hello, Macro! My name is TypeName!`,其中 `TypeName` 为定义了 trait 的类型名。换言之,我们会创建一个 crate使程序员能够写类似示例 19-38 中的代码。
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
```
<span class="caption">示例 19-38: crate 用户所写的能够使用过程式宏的代码</span>
运行该代码将会打印 `Hello, Macro! My name is Pancakes!` 第一步是像下面这样新建一个库 crate
```text
$ cargo new hello_macro --lib
```
接下来,会定义 `HelloMacro` trait 以及其关联函数:
<span class="filename">文件名: src/lib.rs</span>
```rust
pub trait HelloMacro {
fn hello_macro();
}
```
现在有了一个包含函数的 trait 。此时crate 用户可以实现该 trait 以达到其期望的功能,像这样:
```rust,ignore
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
```
然而,他们需要为每一个他们想使用 `hello_macro` 的类型编写实现的代码块。我们希望为其节约这些工作。
另外,我们也无法为 `hello_macro` 函数提供一个能够打印实现了该 trait 的类型的名字的默认实现Rust 没有反射的能力,因此其无法在运行时获取类型名。我们需要一个在运行时生成代码的宏。
下一步是定义过程式宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取消。构造 crate 和其中宏的惯例如下:对于一个 `foo` 的包来说,一个自定义的派生过程宏的包被称为 `foo_derive` 。在 `hello_macro` 项目中新建名为 `hello_macro_derive` 的包。
```text
$ cargo new hello_macro_derive --lib
```
由于两个 crate 紧密相关,因此在 `hello_macro` 包的目录下创建过程式宏的 crate。如果改变在 `hello_macro` 中定义的 trait ,同时也必须改变在 `hello_macro_derive` 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 `hello_macro` 包而将 `hello_macro_derive` 作为一个依赖,并重新导出过程式宏的代码。但我们组织项目的方式使编程人员使用 `hello_macro` 成为可能,即使他们无需 `derive` 的功能。
需要将 `hello_macro_derive` 声明为一个过程宏的 crate。同时也需要 `syn``quote` crate 中的功能,正如注释中所说,需要将其加到依赖中。为 `hello_macro_derive` 将下面的代码加入到 *Cargo.toml* 文件中。
<span class="filename">文件名: hello_macro_derive/Cargo.toml</span>
```toml
[lib]
proc-macro = true
[dependencies]
syn = "0.14.4"
quote = "0.6.3"
```
为定义一个过程式宏,请将示例 19-39 中的代码放在 `hello_macro_derive` crate 的 *src/lib.rs* 文件里面。注意这段代码在我们添加 `impl_hello_macro` 函数的定义之前是无法编译的。
<span class="filename">文件名: hello_macro_derive/src/lib.rs</span>
<!--
This usage of `extern crate` is required for the moment with 1.31.0, see:
https://github.com/rust-lang/rust/issues/54418
https://github.com/rust-lang/rust/pull/54658
https://github.com/rust-lang/rust/issues/55599
-->
> 在 Rust 1.31.0 时,`extern crate` 仍是必须的,请查看 <br />
> https://github.com/rust-lang/rust/issues/54418 <br />
> https://github.com/rust-lang/rust/pull/54658 <br />
> https://github.com/rust-lang/rust/issues/55599
```rust,ignore
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 构建 Rust 代码所代表的语法树
// 以便可以进行操作
let ast = syn::parse(input).unwrap();
// 构建 trait 实现
impl_hello_macro(&ast)
}
```
<span class="caption">示例 19-39: 大多数过程式宏处理 Rust 代码时所需的代码</span>
注意在 19-39 中分离函数的方式,这将和你几乎所见到或创建的每一个过程宏都一样,因为这让编写一个过程式宏更加方便。在 `impl_hello_macro` 被调用的地方所选择做的什么依赖于该过程式宏的目的而有所不同。
现在,我们已经引入了 三个新的 crate`proc_macro` 、 [`syn`] 和 [`quote`] 。Rust 自带 `proc_macro` crate因此无需将其加到 *Cargo.toml* 文件的依赖中。`proc_macro` crate 是编译器用来读取和操作我们 Rust 代码的 API。`syn` crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。`quote` 则将 `syn` 解析的数据结构反过来传入到 Rust 代码中。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
[`syn`]: https://crates.io/crates/syn
[`quote`]: https://crates.io/crates/quote
当用户在一个类型上指定 `#[derive(HelloMacro)]` 时,`hello_macro_derive` 函数将会被调用。原因在于我们已经使用 `proc_macro_derive` 及其指定名称对 `hello_macro_derive` 函数进行了注解:`HelloMacro` ,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
该函数首先将来自 `TokenStream``input` 转换为一个我们可以解释和操作的数据结构。这正是 `syn` 派上用场的地方。`syn` 中的 `parse_derive_input` 函数获取一个 `TokenStream` 并返回一个表示解析出 Rust 代码的 `DeriveInput` 结构体。示例 19-40 展示了从字符串 `struct Pancakes;` 中解析出来的 `DeriveInput` 结构体的相关部分:
```rust,ignore
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
```
<span class="caption">示例 19-40: 解析示例 19-38 中带有宏属性的代码时得到的 `DeriveInput` 实例</span>
该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 `ident` identifier表示名字`Pancakes`。该结构体里面有更多字段描述了所有类型的 Rust 代码,查阅 [`syn` 中 `DeriveInput` 的文档][syn-docs] 以获取更多信息。
[syn-docs]: https://docs.rs/syn/0.14.4/syn/struct.DeriveInput.html
此时,尚未定义 `impl_hello_macro` 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 `TokenStream`。所返回的 `TokenStream` 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。
你可能也注意到了,当调用 `parse_derive_input``parse` 失败时。在错误时 panic 对过程宏来说是必须的,因为 `proc_macro_derive` 函数必须返回 `TokenStream` 而不是 `Result`,以此来符合过程宏的 API。这里选择用 `unwrap` 来简化了这个例子;在生产代码中,则应该通过 `panic!``expect` 来提供关于发生何种错误的更加明确的错误信息。
现在我们有了将注解的 Rust 代码从 `TokenStream` 转换为 `DeriveInput` 实例的代码,让我们来创建在注解类型上实现 `HelloMacro` trait 的代码,如示例 19-41 所示。
<span class="filename">文件名: hello_macro_derive/src/lib.rs</span>
```rust,ignore
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
```
<span class="caption">示例 19-41: 使用解析过的 Rust 代码实现 `HelloMacro` trait</span>
我们得到一个包含以 `ast.ident` 作为注解类型名字(标识符)的 `Ident` 结构体实例。示例 19-40 中的结构体表明当 `impl_hello_macro` 函数运行于示例 19-38 中的代码上时 `ident` 字段的值是 `"Pancakes"`。因此,示例 19-41 中 `name` 变量会包含一个 `Ident` 结构体的实例,当打印时,会是字符串 `"Pancakes"`,也就是示例 19-38 中结构体的名称。
`quote!` 让我们可以编写希望返回的 Rust diamagnetic。`quote!` 宏执行的直接结果并不是编译器所期望的并需要转换为 `TokenStream`。为此需要调用 `into` 方法它会消费这个中间表示intermediate representationIR并返回所需的 `TokenStream` 类型值。
这个宏也提供了一些非常酷的模板机制;我们可以写 `#name` ,然后 `quote!` 会以 名为 `name` 的变量值来替换它。你甚至可以做些与这个正则宏任务类似的重复事情。查阅 [`quote` crate 的文档][quote-docs] 来获取详尽的介绍。
[quote-docs]: https://docs.rs/quote
我们期望我们的过程式宏能够为通过 `#name` 获取到的用户注解类型生成 `HelloMacro` trait 的实现。该 trait 的实现有一个函数 `hello_macro` ,其函数体包括了我们期望提供的功能:打印 `Hello, Macro! My name is` 和注解的类型名。
此处所使用的 `stringify!` 为 Rust 内置宏。其接收一个 Rust 表达式,如 `1 + 2` 然后在编译时将表达式转换为一个字符串常量,如 `"1 + 2"` 。这与 `format!``println!` 是不同的,它计算表达式并将结果转换为 `String` 。有一种可能的情况是,所输入的 `#name` 可能是一个需要打印的表达式,因此我们用 `stringify!``stringify!` 编译时也保留了一份将 `#name` 转换为字符串之后的内存分配。
此时,`cargo build` 应该都能成功编译 `hello_macro``hello_macro_derive` 。我们将这些 crate 连接到示例 19-38 的代码中来看看过程宏的行为!在 *projects* 目录下用 `cargo new pancakes` 命令新建一个二进制项目。需要将 `hello_macro``hello_macro_derive` 作为依赖加到 `pancakes` 包的 *Cargo.toml* 文件中去。如果你正将 `hello_macro``hello_macro_derive` 的版本发布到 *https://crates.io/* 上,其应为正规依赖;如果不是,则可以像下面这样将其指定为 `path` 依赖:
```toml
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
```
把示例 19-38 中的代码放在 *src/main.rs* ,然后执行 `cargo run`:其应该打印 `Hello, Macro! My name is Pancakes!`。其包含了该过程宏中 `HelloMacro` trait 的实现,而无需 `pancakes` crate 实现它;`#[derive(HelloMacro)]` 增加了该 trait 实现。
接下来,让我们探索一下其他类型的过程宏与自定义派生宏有何区别。
### 类属性宏
类属性宏与自定义派生宏相似,不同于为 `derive` 属性生成代码,它们允许你创建新的属性。它们也更为灵活;`derive` 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 `route` 的属性用于注解 web 应用程序框架web application framework的函数
```rust,ignore
#[route(GET, "/")]
fn index() {
```
`#[route]` 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:
```rust,ignore
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
```
这里有两个 `TokenStream` 类型的参数;第一个用于属性内容本身,也就是 `GET, "/"` 部分。第二个是属性所标记的项,在本例中,是 `fn index() {}` 和剩下的函数体。
除此之外,类属性宏与自定义派生宏工作方式一致:创建 `proc-macro` crate 类型的 crate 并实现希望生成代码的函数!
### 类函数宏
最后,类函数宏定义看起来像函数调用的宏。例如,`sql!` 宏可能像这样被调用:
```rust,ignore
let sql = sql!(SELECT * FROM posts WHERE id=1);
```
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的。该宏应该被定义为如此:
```rust,ignore
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
```
这类似于自定义派生宏的签名:获取括号中的 token并返回希望生成的代码。
## 总结
好的!现在我们学习了 Rust 并不常用但在特定情况下你可能用得着的功能。我们介绍了很多复杂的主题,这样若你在错误信息提示或阅读他人代码时遇到他们,至少可以说之前已经见过这些概念和语法了。你可以使用本章作为一个解决方案的参考。
接下来,我们将再开始一个项目,将本书所学的所有内容付与实践!

View File

@ -1,16 +1,16 @@
# 最后的项目: 构建多线程 web server
> [ch20-00-final-project-a-web-server.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-00-final-project-a-web-server.md)
> [ch20-00-final-project-a-web-server.md](https://github.com/rust-lang/book/blob/master/src/ch20-00-final-project-a-web-server.md)
> <br>
> commit e2a38b44f3a7f796fa8000e558dc8dd2ddf340a3
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
这是一次漫长的旅途,不过我们做到了!这一章便是本书的结束。离别是如此甜蜜的悲伤。不过在我们结束之前,再来一起构建另一个项目,来展示最后几章所学,同时复习更早的章节。
这是一次漫长的旅途,不过我们到达了本书的结束。在本章中,我们将一同构建另一个项目,来展示最后几章所学,同时复习更早的章节。
作为最后的项目,我们将要实现一个只返回 “hello” 的 web server它在浏览器中看起来就如图例 20-1 所示:
作为最后的项目,我们将要实现一个返回 “hello” 的 web server它在浏览器中看起来就如图例 20-1 所示:
![hello from rust](img/trpl20-01.png)
<span class="caption">图例 20-1: 我们最将一起分享的项目</span>
<span class="caption">图例 20-1: 我们最将一起分享的项目</span>
如下是我们将怎样构建此 web server 的计划:
@ -20,6 +20,6 @@
4. 创建一个合适的 HTTP 响应
5. 通过线程池改善 server 的吞吐量
不过在开始之前,需要提到一点:这里使用的方法并不是使用 Rust 构建 web server 最好的方法。*https://crates.io* 上有很多可用于生产环境的 crate它们提供了比我们所要编写的更为完整的 web server 和线程池实现。
不过在开始之前,需要提到一点细节:这里使用的方法并不是使用 Rust 构建 web server 最好的方法。*https://crates.io* 上有很多可用于生产环境的 crate它们提供了比我们所要编写的更为完整的 web server 和线程池实现。
然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。因此我们将自己编写一个基础的 HTTP server 和线程池,以便学习将来可能用到的 crate 背后的通用理念和技术。

View File

@ -1,10 +1,10 @@
## 构建单线程 web server
> [ch20-01-single-threaded.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-01-single-threaded.md)
> [ch20-01-single-threaded.md](https://github.com/rust-lang/book/blob/master/src/ch20-01-single-threaded.md)
> <br>
> commit 90e6737d534cb66102674d183d2ef1966b190c2c
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
首先让我们创建一个可运行的单线程 web server不过在开始之前我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供所需的信息。
首先让我们创建一个可运行的单线程 web server不过在开始之前我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供我们所需的信息。
web server 中涉及到的两个主要协议是 **超文本传输协议***Hypertext Transfer Protocol**HTTP*)和 **传输控制协议***Transmission Control Protocol**TCP*)。这两者都是 **请求-响应***request-response*)协议,也就是说,有 **客户端***client*)来初始化请求,并有 **服务端***server*)监听请求并向客户端提供响应。请求与响应的内容由协议本身定义。
@ -15,7 +15,7 @@ TCP 是一个底层协议,它描述了信息如何从一个 server 到另一
所以我们的 web server 所需做的第一件事便是能够监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们一如既往新建一个项目:
```text
$ cargo new hello --bin
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
```
@ -40,28 +40,16 @@ fn main() {
<span class="caption">示例 20-1: 监听传入的流并在接收到流时打印信息</span>
`TcpListener` 用于监听 TCP 连接。我们选择监听地址 `127.0.0.1:7878`。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 接受这个端口而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。注意连接 80 端口需要管理员权限;非管理员用户只能监听大于 1024 的端口。
`TcpListener` 用于监听 TCP 连接。我们选择监听地址 `127.0.0.1:7878`。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 接受这个端口而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。
在这个场景中 `bind` 函数类似于 `new` 函数,在这里它返回一个新的 `TcpListener` 实例。这个函数叫做 `bind` 是因为,在网络领域,连接到监听端口被称为 “绑定到一个端口”“binding to a port”
`bind` 函数返回 `Result<T, E>`,这表明绑定可能会失败,例如,如果不是管理员尝试连接 80 端口,或是如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的 server将不用关心处理这类错误使用 `unwrap` 在出现这些情况时直接停止程序。
`bind` 函数返回 `Result<T, E>`,这表明绑定可能会失败,例如,连接 80 端口需要管理员权限(非管理员用户只能监听大于 1024 的端口),所以如果不是管理员尝试连接 80 端口,则会绑定失败。另一个例子是如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的 server将不用关心处理这类错误使用 `unwrap` 在出现这些情况时直接停止程序。
`TcpListener``incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流***stream*)代表一个客户端和服务端之间打开的连接。**连接***connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,`TcpStream` 允许我们读取它来查看客户端发送了什么,并可以编写响应。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。
<!-- Below -- What if there aren't errors, how is the stream handled? Or is
there no functionality for that yet, only functionality for errors?
Also, highlighted below -- can you specify what errors we're talking
about---errors in *producing* the streams or connecting to the port?-->
<!--
There is no functionality for a stream without errors yet; I've clarified.
The errors happen when a client attempts to connect to the server; I've
clarified.
/Carol -->
目前为止,处理流的过程包含 `unwrap` 调用,如果出现任何错误会终止程序,如果没有任何错误,则打印出信息。下一个示例我们将为成功的情况增加更多功能。当客户端连接到服务端时 `incoming` 方法返回错误是可能的,因为我们实际上没有遍历连接,而是遍历 **连接尝试***connection attempts*)。连接可能会因为很多原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数;新连接尝试产生错误,直到一些打开的连接关闭为止。
让我们试试这段代码!首先在终端执行 `cargo run`,接着在浏览器中加载 `127.0.0.1:7878`。浏览器会显示出看起来类似于“连接重置”“Connection reset”的错误信息因为 server 目前并没响应任何数据。但是如果我们观察终端,会发现当浏览器连接 server 时会打印出一系列的信息!
```text
@ -71,11 +59,11 @@ Connection established!
Connection established!
```
有时会看到对于一次浏览器请求会打印出多条信息;这可能是因为浏览器在请求页面的同时还请求了其他资源,比如出现在浏览器 tab 标签中的 `favicon.ico`
有时会看到对于一次浏览器请求会打印出多条信息;这可能是因为浏览器在请求页面的同时还请求了其他资源,比如出现在浏览器 tab 标签中的 *favicon.ico*
这也可能是因为浏览器尝试多次连接 server因为 server 没有响应任何数据。当 `stream` 在循环的结尾离开作用域并被丢弃,其连接将被关闭,作为 `drop` 实现的一部分。浏览器有时通过重连来处理关闭的连接,因为这些问题可能是暂时的。现在重要的是我们成功的处理了 TCP 连接!
记得当运行完特定版本的代码后使用 <span class="keystroke">ctrl-C</span> 来停止程序并在做出最新的代码修改之后执行 `cargo run` 重启服务。
记得当运行完特定版本的代码后使用 <span class="keystroke">ctrl-C</span> 来停止程序并在做出最新的代码修改之后执行 `cargo run` 重启服务。
### 读取请求
@ -85,8 +73,8 @@ Connection established!
```rust,no_run
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
@ -111,18 +99,11 @@ fn handle_connection(mut stream: TcpStream) {
这里将 `std::io::prelude` 引入作用域来获取读写流所需的特定 trait。在 `main` 函数的 `for` 循环中,相比获取到连接时打印信息,现在调用新的 `handle_connection` 函数并向其传递 `stream`
`handle_connection` 中,`stream` 参数是可变的。
我们将从流中读取数据,所以它需要是可修改的。这是因为 `TcpStream` 实例在内部记录了所返回的数据。它可能读取了多于我们请求的数据并保存它们以备下一次请求数据。因此它需要是 `mut` 的因为其内部状态可能会改变;通常我们认为 “读取” 不需要可变性,不过在这个例子中则需要 `mut` 关键字。
<!-- Above -- I'm not clear what state will change here, the content of stream
when the program tempers what data it takes? -->
<!-- Yes, which is what we mean by "internally". I've tried to reword a bit,
not sure if it's clearer. /Carol -->
`handle_connection` 中,`stream` 参数是可变的。这是因为 `TcpStream` 实例在内部记录了所返回的数据。它可能读取了多于我们请求的数据并保存它们以备下一次请求数据。因此它需要是 `mut` 的因为其内部状态可能会改变;通常我们认为 “读取” 不需要可变性,不过在这个例子中则需要 `mut` 关键字。
接下来,需要实际读取流。这里分两步进行:首先,在栈上声明一个 `buffer` 来存放读取到的数据。这里创建了一个 512 字节的缓冲区,它足以存放基本请求的数据并满足本章的目的需要。如果希望处理任意大小的请求,缓冲区管理将更为复杂,不过现在一切从简。接着将缓冲区传递给 `stream.read` ,它会从 `TcpStream` 中读取字节并放入缓冲区中。
接下来将缓冲区中的字节转换为字符串并打印出来。`String::from_utf8_lossy` 函数获取一个 `&[u8]` 并产生一个 `String`。函数名的 “lossy” 部分来源于当其遇到无效的 UTF-8 序列时的行为:它使用 <20>`U+FFFD REPLACEMENT CHARACTER`,来代替无效序列。你可能会在缓冲区的剩余部分看到这些替代字符,因为他们没有被请求数据填满。
接下来将缓冲区中的字节转换为字符串并打印出来。`String::from_utf8_lossy` 函数获取一个 `&[u8]` 并产生一个 `String`。函数名的 “lossy” 部分来源于当其遇到无效的 UTF-8 序列时的行为:它使用 `<60>``U+FFFD REPLACEMENT CHARACTER`,来代替无效序列。你可能会在缓冲区的剩余部分看到这些替代字符,因为他们没有被请求数据填满。
让我们试一试!启动程序并再次在浏览器中发起请求。注意浏览器中仍然会出现错误页面,不过终端中程序的输出现在看起来像这样:
@ -143,7 +124,7 @@ Upgrade-Insecure-Requests: 1
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
```
根据使用的浏览器不同可能会出现稍微不同的数据。现在我们打印出了请求数据,可以通过观察 `Request: GET` 之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 `/`,就知道了浏览器尝试重复获取 `/` 因为它没有从程序得到响应。
根据使用的浏览器不同可能会出现稍微不同的数据。现在我们打印出了请求数据,可以通过观察 `Request: GET` 之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 */*,就知道了浏览器尝试重复获取 */* 因为它没有从程序得到响应。
拆开请求数据来理解浏览器向程序请求了什么。
@ -159,19 +140,11 @@ message-body
第一行叫做 **请求行***request line*),它存放了客户端请求了什么的信息。请求行的第一部分是所使用的 *method*,比如 `GET``POST`,这描述了客户端如何进行请求。这里客户端使用了 `GET` 请求。
<!-- Below, is that right that the / part is the URI *being requested*, and not
the URI of the requester? -->
<!-- Yes /Carol -->
请求行接下来的部分是 */*,它代表客户端请求的 **统一资源标识符***Uniform Resource Identifier**URI* —— URI 大体上类似,但也不完全类似于 URL**统一资源定位符***Uniform Resource Locators*。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI所以这里可以简单的将 URL 理解为 URI。
`Request` 行接下来的部分是 `/`,它代表客户端请求的 **统一资源标识符***Uniform Resource Identifier**URI* —— URI 大体上类似,但也不完全类似于 URL**统一资源定位符***Uniform Resource Locators*。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI所以这里可以简单的将 URL 理解为 URI
最后,是客户端使用的 HTTP 版本,接着请求行以一个 **CRLF 序列**CRLF 是**回车***carriage return* 和 **换行***line feed* 的缩写这些术语来自打字机时代结尾。结尾。CRLF 序列也可以写作 `\r\n``\r` 是回车而 `\n` 是换行。CRLF 序列将请求行与其余的请求数据分开。注意当 CRLF 被打印时,会看到开始了一个新行而不是 `\r\n`。
最后,是客户端使用的 HTTP 版本,接着请求行以一个 CRLF 序列结尾。CRLF 序列也可以写作 `\r\n``\r` 是 **回车***carriage return*)而 `\n` 是 **换行***line feed*)(这些术语来自打字机时代!)。注意当 CRLF 被打印时,会看到开始了一个新行而不是 `\r\n`。
<!-- Above, I don't see a CRLF here in the request line in the actual output,
is it just implied because the next line begins on the next line? -->
<!-- Yes, I've clarified. /Carol -->
观察目前运行程序所接收到的数据的请求行,可以看到 `GET` 是 method`/` 是请求 URI`HTTP/1.1` 是版本。
观察目前运行程序所接收到的数据的请求行,可以看到 `GET` 是 method*/* 是请求 URI`HTTP/1.1` 是版本。
`Host:` 开始的其余的行是 headers`GET` 请求没有 body。
@ -191,15 +164,13 @@ message-body
第一行叫做 **状态行***status line*),它包含响应的 HTTP 版本、一个数字状态码用以总结请求的结果和一个描述之前状态码的文本原因短语。CRLF 序列之后是任意 header另一个 CRLF 序列,和响应的 body。
这里是一个使用 HTTP 1.1 版本的响应例子,其状态码为 `200`,原因短语为 `OK`,没有 header也没有 body
这里是一个使用 HTTP 1.1 版本的响应例子,其状态码为 200原因短语为 OK没有 header也没有 body
```text
HTTP/1.1 200 OK\r\n\r\n
```
状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应!
`handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 20-3 中的代码:
状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应!在 `handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 20-3 中的代码:
<span class="filename">文件名: src/main.rs</span>
@ -220,26 +191,11 @@ fn handle_connection(mut stream: TcpStream) {
<span class="caption">示例 20-3: 将一个微型成功 HTTP 响应写入流</span>
<!-- Flagging for addition of wingdings later -->
新代码中的第一行定义了变量 `response` 来存放将要返回的成功响应的数据。接着,在 `response` 上调用 `as_bytes`,因为 `stream``write` 方法获取一个 `&[u8]` 并直接将这些字节发送给连接。
<!-- Above--So what does adding as_bytes actually do, *allow* us to send bytes
directly? -->
<!-- It converts the string data to bytes, I've clarified /Carol -->
因为 `write` 操作可能会失败,所以像之前那样对任何错误结果使用 `unwrap`。同理,在真实世界的应用中这里需要添加错误处理。最后,`flush` 会等待并阻塞程序执行直到所有字节都被写入连接中;`TcpStream` 包含一个内部缓冲区来最小化对底层操作系统的调用。
<!-- Above -- Will flush wait until all bytes are written and then do
something? I'm not sure what task it's performing -->
<!-- `flush` just makes sure all the bytes we sent to `write` are actually
written to the stream before the function ends. Because writing to a stream
takes time, the `handle_connection` function could potentially finish and
`stream` could go out of scope before all the bytes given to `write` are sent,
unless we call `flush`. This is how streams work in many languages and is a
small detail I don't think is worth going into in depth. /Carol -->
有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 `127.0.0.1:7878` 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。
有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 *127.0.0.1:7878* 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。
### 返回真正的 HTML
@ -263,25 +219,21 @@ small detail I don't think is worth going into in depth. /Carol -->
<span class="caption">示例 20-4: 一个简单的 HTML 文件用来作为响应</span>
这是一个极小化的 HTML 5 文档,它有一个标题和一小段文本。为了在 server 接受请求时返回它,需要如示例 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中,并发送:
这是一个极小化的 HTML5 文档,它有一个标题和一小段文本。为了在 server 接受请求时返回它,需要如示例 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中,并发送:
<span class="filename">文件名: src/main.rs</span>
```rust
# use std::io::prelude::*;
# use std::net::TcpStream;
use std::fs::File;
use std::fs;
// --snip--
fn handle_connection(mut stream: TcpStream) {
let mut buffer = [0; 512];
stream.read(&mut buffer).unwrap();
let mut file = File::open("hello.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let contents = fs::read_to_string("hello.html").unwrap();
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
@ -296,20 +248,20 @@ fn handle_connection(mut stream: TcpStream) {
接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。
使用 `cargo run` 运行程序,在浏览器加载 `127.0.0.1:7878`,你应该会看到渲染出来的 HTML 文件!
使用 `cargo run` 运行程序,在浏览器加载 *127.0.0.1:7878*,你应该会看到渲染出来的 HTML 文件!
目前忽略了 `buffer` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 `127.0.0.1:7878/something-else` 也会得到同样的 HTML 响应。如此其作用是非常有限的,也不是大部分 server 所做的让我们检查请求并只对格式良好well-formed的请求 `/` 发送 HTML 文件。
目前忽略了 `buffer` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 *127.0.0.1:7878/something-else* 也会得到同样的 HTML 响应。如此其作用是非常有限的,也不是大部分 server 所做的让我们检查请求并只对格式良好well-formed的请求 `/` 发送 HTML 文件。
### 验证请求并有选择的进行响应
目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 `/`,并在其请求任何其他内容时返回错误的功能。为此需要如示例 20-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 `/` 请求的一部分做比较,并增加了 `if``else` 块来区别处理请求:
目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 */*,并在其请求任何其他内容时返回错误的功能。为此需要如示例 20-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 */* 请求的一部分做比较,并增加了 `if``else` 块来区别处理请求:
<span class="filename">文件名: src/main.rs</span>
```rust
# use std::io::prelude::*;
# use std::net::TcpStream;
# use std::fs::File;
# use std::fs;
// --snip--
fn handle_connection(mut stream: TcpStream) {
@ -319,48 +271,41 @@ fn handle_connection(mut stream: TcpStream) {
let get = b"GET / HTTP/1.1\r\n";
if buffer.starts_with(get) {
let mut file = File::open("hello.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let contents = fs::read_to_string("hello.html").unwrap();
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
} else {
// some other request
// 其他请求
}
}
```
<span class="caption">示例 20-6: 匹配请求并区别处理 `/` 请求与其他请求</span>
首先,将与 `/` 请求相关的数据硬编码进变量 `get`。因为我们将原始字节读取进了缓冲区,所以在 `get` 的数据开头增加 `b""` 字节字符串语法将其转换为字节字符串。接着检查 `buffer` 是否以 `get` 中的字节开头。如果是,这就是一个格式良好的 `/` 请求,也就是 `if` 块中期望处理的成功情况,并会返回 HTML 文件内容的代码。
首先,将与 */* 请求相关的数据硬编码进变量 `get`。因为我们将原始字节读取进了缓冲区,所以在 `get` 的数据开头增加 `b""` 字节字符串语法将其转换为字节字符串。接着检查 `buffer` 是否以 `get` 中的字节开头。如果是,这就是一个格式良好的 */* 请求,也就是 `if` 块中期望处理的成功情况,并会返回 HTML 文件内容的代码。
如果 `buffer` **不**`get` 中的字节开头,就说明接收的是其他请求。之后会在 `else` 块中增加代码来响应所有其他请求。
现在如果运行代码并请求 `127.0.0.1:7878`,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求比如 `127.0.0.1:7878/something-else`,则会得到像运行示例 20-1 和 20-2 中代码那样的连接错误。
现在如果运行代码并请求 *127.0.0.1:7878*,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求比如 *127.0.0.1:7878/something-else*,则会得到像运行示例 20-1 和 20-2 中代码那样的连接错误。
现在向示例 20-7 的 `else` 块增加代码来返回一个带有 `404` 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意:
现在向示例 20-7 的 `else` 块增加代码来返回一个带有 404 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意:
<span class="filename">文件名: src/main.rs</span>
```rust
# use std::io::prelude::*;
# use std::net::TcpStream;
# use std::fs::File;
# use std::fs;
# fn handle_connection(mut stream: TcpStream) {
# if true {
// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
let mut file = File::open("404.html").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let contents = fs::read_to_string("404.html").unwrap();
let response = format!("{}{}", status_line, contents);
@ -370,9 +315,9 @@ fn handle_connection(mut stream: TcpStream) {
# }
```
<span class="caption">示例 20-7: 对于任何不是 `/` 的请求返回 `404` 状态码的响应和错误页面</span>
<span class="caption">示例 20-7: 对于任何不是 */* 的请求返回 `404` 状态码的响应和错误页面</span>
这里,响应的状态行有状态码 `404` 和原因短语 `NOT FOUND`。仍然没有返回任何 header而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 20-8 中的示例 HTML
这里,响应的状态行有状态码 404 和原因短语 `NOT FOUND`。仍然没有返回任何 header而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 20-8 中的示例 HTML
<span class="filename">文件名: 404.html</span>
@ -390,9 +335,9 @@ fn handle_connection(mut stream: TcpStream) {
</html>
```
<span class="caption">示例 20-8: 任何 `404` 响应所返回错误页面内容样例</span>
<span class="caption">示例 20-8: 任何 404 响应所返回错误页面内容样例</span>
有了这些修改,再次运行 server。请求 `127.0.0.1:7878` 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 `127.0.0.1:7878/foo`,应该会返回 *404.html* 中的错误 HTML
有了这些修改,再次运行 server。请求 *127.0.0.1:7878* 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 *127.0.0.1:7878/foo*,应该会返回 *404.html* 中的错误 HTML
### 少量代码重构
@ -403,7 +348,7 @@ fn handle_connection(mut stream: TcpStream) {
```rust
# use std::io::prelude::*;
# use std::net::TcpStream;
# use std::fs::File;
# use std::fs;
// --snip--
fn handle_connection(mut stream: TcpStream) {
@ -419,10 +364,7 @@ fn handle_connection(mut stream: TcpStream) {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let mut file = File::open(filename).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let contents = fs::read_to_string(filename).unwrap();
let response = format!("{}{}", status_line, contents);
@ -437,6 +379,6 @@ fn handle_connection(mut stream: TcpStream) {
之前读取文件和写入响应的冗余代码现在位于 `if``else` 块之外,并会使用变量 `status_line``filename`。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例 20-9 中代码的行为与示例 20-8 完全一样。
好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server它对一个请求返回页面内容而对所有其他请求返回 `404` 响应。
好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server它对一个请求返回页面内容而对所有其他请求返回 404 响应。
目前 server 运行于单线程中,它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会成为一个问题,并进行修复以便 server 可以一次处理多个请求。

View File

@ -1,19 +1,14 @@
## 将单线程 server 变为多线程 server
> [ch20-02-multithreaded.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-02-multithreaded.md)
> [ch20-02-multithreaded.md](https://github.com/rust-lang/book/blob/master/src/ch20-02-multithreaded.md)
> <br>
> commit 1f0136399ba2f5540ecc301fab04bd36492e5554
<!-- Reading ahead, the original heading didn't seem to fit all of the sub
headings -- this might not be totally right either, so feel free to replace
with something more appropriate -->
<!-- This is fine! /Carol -->
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
目前 server 会依次处理每一个请求,意味着它在完成第一个连接的处理之前不会处理第二个连接。如果 server 正接收越来越多的请求,这类串行操作会使性能越来越差。如果一个请求花费很长时间来处理,随后而来的请求则不得不等待这个长请求结束,即便这些新请求可以很快就处理完。我们需要修复这种情况,不过首先让我们实际尝试一下这个问题。
### 在当前 server 实现中模拟慢请求
让我们看看一个慢请求如何影响当前 server 实现中的其他请求。示例 20-10 通过模拟慢响应实现了 `/sleep` 请求处理,它会使 server 在响应之前休眠五秒。
让我们看看一个慢请求如何影响当前 server 实现中的其他请求。示例 20-10 通过模拟慢响应实现了 */sleep* 请求处理,它会使 server 在响应之前休眠五秒。
<span class="filename">文件名: src/main.rs</span>
@ -46,23 +41,18 @@ fn handle_connection(mut stream: TcpStream) {
}
```
<span class="caption">示例 20-10: 通过识别 `/sleep` 并休眠五秒来模拟慢请求</span>
<span class="caption">示例 20-10: 通过识别 */sleep* 并休眠五秒来模拟慢请求</span>
这段代码有些凌乱,不过对于模拟的目的来说已经足够这里创建了第二个请求 `sleep`,我们会识别其数据。在 `if` 块之后增加了一个 `else if` 来检查 `/sleep` 请求,当接收到这个请求时,在渲染成功 HTML 页面之前会先休眠五秒。
这段代码有些凌乱,不过对于模拟的目的来说已经足够这里创建了第二个请求 `sleep`,我们会识别其数据。在 `if` 块之后增加了一个 `else if` 来检查 */sleep* 请求,当接收到这个请求时,在渲染成功 HTML 页面之前会先休眠五秒。
现在就可以真切的看出我们的 server 有多么的原始;真实的库将会以更简洁的方式处理多请求识别问题
现在就可以真切的看出我们的 server 有多么的原始;真实的库将会以更简洁的方式处理多请求识别问题
使用 `cargo run` 启动 server并接着打开两个浏览器窗口一个请求 `http://localhost:7878/` 而另一个请求 `http://localhost:7878/sleep`。如果像之前一样多次请求 `/`,会发现响应的比较快速。不过如果请求`/sleep` 之后在请求 `/`,就会看到 `/` 会等待直到 `sleep` 休眠完五秒之后才出现。
使用 `cargo run` 启动 server并接着打开两个浏览器窗口一个请求 *http://127.0.0.1:7878/* 而另一个请求 *http://127.0.0.1:7878/sleep*。如果像之前一样多次请求 */*,会发现响应的比较快速。不过如果请求 */sleep* 之后在请求 */*,就会看到 */* 会等待直到 `sleep` 休眠完五秒之后才出现。
这里有多种办法来改变我们的 web server 使其避免所有请求都排在慢请求之后;我们将要实现的一个便是线程池。
### 使用线程池改善吞吐量
<!--There seems to be some repetition throughout these thread pool sections, is
there any way to condense it? I've edited with this in mind, but am wary of
changing too much -->
<!-- Your edits that removed repetition are fine! /Carol -->
**线程池***thread pool*)是一组预先分配的等待或准备处理任务的线程。当程序收到一个新任务,线程池中的一个线程会被分配任务,这个线程会离开并处理任务。其余的线程则可用于处理在第一个线程处理任务的同时处理其他接收到的任务。当第一个线程处理完任务时,它会返回空闲线程池中等待处理新任务。线程池允许我们并发处理连接,增加 server 的吞吐量。
我们会将池中线程限制为较少的数量以防拒绝服务Denial of Service DoS攻击如果程序为每一个接收的请求都新建一个线程某人向 server 发起千万级的请求时会耗尽服务器的资源并导致所有请求的处理都被终止。
@ -73,7 +63,7 @@ changing too much -->
在开始之前,让我们讨论一下线程池应用看起来怎样。当尝试设计代码时,首先编写客户端接口确实有助于指导代码设计。以期望的调用方式来构建 API 代码的结构,接着在这个结构之内实现功能,而不是先实现功能再设计公有 API。
类似于第十二章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(Compiler Driven Development。我们将编写调用所期望的函数的代码接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。
类似于第十二章项目中使用的测试驱动开发。这里将要使用编译器驱动开发(compiler-driven development。我们将编写调用所期望的函数的代码接着观察编译器错误告诉我们接下来需要修改什么使得代码可以工作。
#### 为每一个请求分配线程的代码结构
@ -103,7 +93,7 @@ fn main() {
<span class="caption">示例 20-11: 为每一个流新建一个线程</span>
正如第十六章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 `/sleep`,接着在另两个浏览器标签页中加载 `/`,确实会发现 `/` 请求不必等待 `/sleep` 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。
正如第十六章讲到的,`thread::spawn` 会创建一个新线程并在其中运行闭包中的代码。如果运行这段代码并在在浏览器中加载 */sleep*,接着在另两个浏览器标签页中加载 */*,确实会发现 */* 请求不必等待 */sleep* 结束。不过正如之前提到的,这最终会使系统崩溃因为我们无限制的创建新线程。
#### 为有限数量的线程创建一个类似的接口
@ -142,10 +132,6 @@ fn main() {
这里使用 `ThreadPool::new` 来创建一个新的线程池,它有一个可配置的线程数的参数,在这里是四。这样在 `for` 循环中,`pool.execute` 有着类似 `thread::spawn` 的接口,它获取一个线程池运行于每一个流的闭包。`pool.execute` 需要实现为获取闭包并传递给池中的线程运行。这段代码还不能编译,不过通过尝试编译器会指导我们如何修复它。
<!-- Can you be more specific here about how pool.execute will work? -->
<!-- So clarified. I hope this helps with some of the future confusion as well
/Carol -->
#### 采用编译器驱动构建 `ThreadPool` 结构体
继续并对示例 20-12 中的 *src/main.rs* 做出修改,并利用来自 `cargo check` 的编译器错误来驱动开发。下面是我们得到的第一个错误:
@ -178,7 +164,6 @@ pub struct ThreadPool;
<span class="filename">文件名: src/bin/main.rs</span>
```rust,ignore
extern crate hello;
use hello::ThreadPool;
```
@ -196,7 +181,7 @@ error[E0599]: no function or associated item named `new` found for type
`hello::ThreadPool`
```
好的,这告诉我们下一步是为 `ThreadPool` 创建一个叫做 `new` 的关联函数。我们还知道 `new` 需要有一个参数可以接受 `4`,而且 `new` 应该返回 `ThreadPool` 实例。让我们实现拥有此特征的最小化 `new` 函数:
这告诉我们下一步是为 `ThreadPool` 创建一个叫做 `new` 的关联函数。我们还知道 `new` 需要有一个参数可以接受 `4`,而且 `new` 应该返回 `ThreadPool` 实例。让我们实现拥有此特征的最小化 `new` 函数:
<span class="filename">文件夹: src/lib.rs</span>
@ -233,14 +218,6 @@ error[E0599]: no method named `execute` found for type `hello::ThreadPool` in th
| ^^^^^^^
```
<!--Can you say a few words on why we would need an execute method, what Rust
needs it for? Also why we need a closure/what indicated that we need a closure
here? -->
<!-- *Rust* doesn't need it, the thread pool functionality we're working on
implementing needs it. I've tried to clarify without getting too repetitive
with the "Creating a Similar Interface for a Finite Number of Threads" section
/Carol -->
现在有了一个警告和一个错误。暂时先忽略警告,发生错误是因为并没有 `ThreadPool` 上的 `execute` 方法。回忆 “为有限数量的线程创建一个类似的接口” 部分我们决定线程池应该有与 `thread::spawn` 类似的接口,同时我们将实现 `execute` 函数来获取传递的闭包并将其传递给池中的空闲线程执行。
我们会在 `ThreadPool` 上定义 `execute` 函数来获取一个闭包参数。回忆第十三章的 “使用带有泛型和 `Fn` trait 的闭包” 部分,闭包作为参数时可以使用三个不同的 trait`Fn`、`FnMut` 和 `FnOnce`。我们需要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 `thread::spawn`,所以我们可以观察 `thread::spawn` 的签名在其参数中使用了何种 bound。查看文档会发现
@ -254,11 +231,6 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
`F` 是这里我们关心的参数;`T` 与返回值有关所以我们并不关心。考虑到 `spawn` 使用 `FnOnce` 作为 `F` 的 trait bound这可能也是我们需要的因为最终会将传递给 `execute` 的参数传给 `spawn`。因为处理请求的线程只会执行闭包一次,这也进一步确认了 `FnOnce` 是我们需要的 trait这里符合 `FnOnce``Once` 的意思。
<!-- Above -- why does that second reason mean FnOnce is the trait to use, can
you remind us? -->
<!-- Attempted, we're just pointing out that it's in the name Fn*Once* /Carol
-->
`F` 还有 trait bound `Send` 和生命周期绑定 `'static`,这对我们的情况也是有意义的:需要 `Send` 来将闭包从一个线程转移到另一个线程,而 `'static` 是因为并不知道线程会执行多久。让我们编写一个使用带有这些 bound 的泛型参数 `F``ThreadPool``execute` 方法:
<span class="filename">文件名: src/lib.rs</span>
@ -279,7 +251,7 @@ impl ThreadPool {
`FnOnce` trait 仍然需要之后的 `()`,因为这里的 `FnOnce` 代表一个没有参数也没有返回值的闭包。正如函数的定义,返回值类型可以从签名中省略,不过即便没有参数也需要括号。
这里再一次增加了 `execute` 方法的最小化实现,它没有做任何工作。再次进行检查:
这里再一次增加了 `execute` 方法的最小化实现:它没有做任何工作,只是尝试让代码能够编译。再次进行检查:
```text
$ cargo check
@ -308,25 +280,20 @@ warning: unused variable: `f`
#### 在 `new` 中验证池中线程数量
这里仍然存在警告是因为其并没有对 `new``execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。
之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `u32` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic如示例 20-13 所示:
在返回 `ThreadPool` 之前检查 `size` 是否大于零,并使用 `assert!` 宏在得到零时 panic如列表 20-13 所示:
这里仍然存在警告是因为其并没有对 `new``execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `u32` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic如示例 20-13 所示:
<span class="filename">文件名: src/lib.rs</span>
```rust
# pub struct ThreadPool;
impl ThreadPool {
/// Create a new ThreadPool.
/// 创建线程池。
///
/// The size is the number of threads in the pool.
/// 线程池中线程的数量。
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
/// `new` 函数在 size 为 0 时会 panic。
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
@ -339,19 +306,17 @@ impl ThreadPool {
<span class="caption">示例 20-13: 实现 `ThreadPool::new``size` 为零时 panic</span>
趁着这个机会我们用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何!
这里用文档注释为 `ThreadPool` 增加了一些文档。注意这里遵循了良好的文档实践并增加了一个部分来提示函数会 panic 的情况,正如第十四章所讨论的。尝试运行 `cargo doc --open` 并点击 `ThreadPool` 结构体来查看生成的 `new` 的文档看起来如何!
相比像这里使用 `assert!` 宏,也可以让 `new` 像之前 I/O 项目中示例 12-9 中 `Config::new` 那样返回一个 `Result`,不过在这里我们选择创建一个没有任何线程的线程池应该是不可恢复的错误。如果你想做的更好,尝试编写一个采用如下签名的 `new` 版本来感受一下两者的区别:
```rust,ignore
fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {
pub fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {
```
#### 分配空间以储存线程
现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回之前将他们储存在 `ThreadPool` 结构体中。
这引出了另一个问题:如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名:
现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回之前将他们储存在 `ThreadPool` 结构体中。不过如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名:
```rust,ignore
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
@ -366,7 +331,7 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
```rust,ignore,not_desired_behavior
use std::thread;
pub struct ThreadPool {
@ -403,32 +368,11 @@ impl ThreadPool {
#### `Worker` 结构体负责从 `ThreadPool` 中将代码传递给线程
<!-- I wasn't sure what this next paragraph was relevant to, can you connect it
up more clearly?-->
<!-- This is where we're actually getting into the meat of the implementation,
I've tried to make it clearer :( /Carol-->
示例 20-14 的 `for` 循环中留下了一个关于创建线程的注释。如何实际创建线程呢?这是一个难题。标准库提供的创建线程的方法,`thread::spawn`,它期望获取一些一旦创建线程就应该执行的代码。然而,我们希望开始线程并使其等待稍后传递的代码。标准库的线程实现并没有包含这么做的方法;我们必须自己实现。
<!-- Can you say how doing this refactoring will improve the code -- why don't
we want the pool to store threads directly? (I got that from the listing
caption because I wasn't sure what the end game was) -->
<!-- I hope the end game is now clearer in the previous paragraph: we *can't*
store the threads directly and get the behavior we want. /Carol -->
我们将要实现的行为是创建线程并稍后发送代码,这会在 `ThreadPool` 和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 `Worker`:这是一个池实现中的常见概念。想象一下在餐馆厨房工作的员工:员工等待来自客户的订单,他们负责接受这些订单并完成它们。
<!-- I was unclear on what a worker actually is here -- is this a
programming/Rust term, or just what we're calling the struct? Can you make it
clearer what the worker is and its responsibilities? -->
<!-- I've tried in the previous paragraph; it's a common term in job
queue/pooling implementations in programming in general but I think should make
sense in plain English with the real-life metaphor I've added /Carol -->
不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在
`Worker` 上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker `id`,这样就可以在日志和调试中区别线程池中的不同 worker。
不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在 `Worker` 上实现一个方法,它会获取需要允许代码的闭包并将其发送给已经运行的线程执行。我们还会赋予每一个 worker `id`,这样就可以在日志和调试中区别线程池中的不同 worker。
首先,让我们做出如此创建 `ThreadPool` 时所需的修改。在通过如下方式设置完 `Worker` 之后,我们会实现向线程发送闭包的代码:
@ -507,7 +451,7 @@ impl Worker {
4. `execute` 方法会在通道发送端发出期望执行的任务。
5. 在线程中,`Worker` 会遍历通道的接收端并执行任何接收到的任务。
让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在通道中发出的类型目前它是一个没有任何内容的结构体:
让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在通道中发出的类型目前它是一个没有任何内容的结构体:
<span class="filename">文件名: src/lib.rs</span>
@ -569,7 +513,7 @@ impl ThreadPool {
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
@ -629,10 +573,6 @@ error[E0382]: use of moved value: `receiver`
这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的回忆第十六章Rust 所提供的通道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆通道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。
<!-- Above - you may be able to tell I struggled to follow this explanation,
can you double check my edits and correct here? -->
<!-- Yep, the text we had here was nonsensical. The edits are fine! /Carol -->
另外,从通道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。
回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc<Mutex<T>>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改:
@ -644,7 +584,6 @@ can you double check my edits and correct here? -->
# use std::sync::mpsc;
use std::sync::Arc;
use std::sync::Mutex;
// --snip--
# pub struct ThreadPool {
@ -744,7 +683,7 @@ impl ThreadPool {
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
// --snip--
impl Worker {
@ -773,7 +712,7 @@ impl Worker {
如果锁定了互斥器,接着调用 `recv` 从通道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有通道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。
调用 `recv` **阻塞** 当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex<T>` 确保一次只有一个 `Worker` 线程尝试请求任务。
调用 `recv` 会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex<T>` 确保一次只有一个 `Worker` 线程尝试请求任务。
理论上这段代码应该能够编译。不幸的是Rust 编译器仍不够完美,会给出如下错误:
@ -838,9 +777,7 @@ impl Worker {
接下来,为任何实现了 `FnOnce()` trait 的类型 `F` 实现 `FnBox` trait。这实际上意味着任何 `FnOnce()` 闭包都可以使用 `call_box` 方法。`call_box` 的实现使用 `(*self)()` 将闭包移动出 `Box<T>` 并调用此闭包。
现在我们需要 `Job` 类型别名是任何实现了新 trait `FnBox``Box`。这允许我们在得到 `Job` 值时使用 `Worker` 中的 `call_box`。为任何 `FnOnce()` 闭包都实现了 `FnBox` trait 意味着无需对实际在通道中发出的值做任何修改。
最后,对于 `Worker::new` 的线程中所运行的闭包,调用 `call_box` 而不是直接执行闭包。现在 Rust 就能够理解我们的行为是正确的了。
现在我们需要 `Job` 类型别名是任何实现了新 trait `FnBox``Box`。这允许我们在得到 `Job` 值时使用 `Worker` 中的 `call_box`。为任何 `FnOnce()` 闭包都实现了 `FnBox` trait 意味着无需对实际在通道中发出的值做任何修改。现在 Rust 就能够理解我们的行为是正确的了。
这是非常狡猾且复杂的手段。无需过分担心他们并不是非常有道理;总有一天,这一切将是毫无必要的。
@ -875,7 +812,7 @@ warning: field is never used: `thread`
Finished dev [unoptimized + debuginfo] target(s) in 0.99 secs
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
@ -887,13 +824,15 @@ Worker 0 got a job; executing.
Worker 2 got a job; executing.
```
成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 `/sleep`server 也能够通过另外一个线程处理其他请求。
成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 */sleep*server 也能够通过另外一个线程处理其他请求。
注意如果同时在多个浏览器窗口打开 */sleep*,它们可能会彼此间隔地加载 5 秒,因为一些浏览器处于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 web server 造成的。
在学习了第十八章的 `while let` 循环之后,你可能会好奇为何不能如此编写 worker 线程:
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
```rust,ignore,not_desired_behavior
// --snip--
impl Worker {
@ -916,6 +855,6 @@ impl Worker {
<span class="caption">示例 20-22: 一个使用 `while let``Worker::new` 替代实现</span>
这段代码可以编译和运行,但是并不会产生所期望的线程行为:一个慢请求仍然会导致其他请求等待执行。如此的原因有些微妙:`Mutex` 结构体没有公有 `unlock` 方法,因为锁的所有权依赖 `lock` 方法返回的 `LockResult<MutexGuard<T>>``MutexGuard<T>` 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 `Mutex` 守护的资源,不过如果没有认真的思考 `MutexGuard<T>` 的生命周期的话,也可能会导致比预期更久的持有锁。因为 `while` 表达式中的值在整个块一直处于作用域中,`job.call_box()` 调用的过程中其仍然持有锁,这意味着其他 worker 不能接收任务。
这段代码可以编译和运行,但是并不会产生所期望的线程行为:一个慢请求仍然会导致其他请求等待执行。原因有些微妙:`Mutex` 结构体没有公有 `unlock` 方法,因为锁的所有权依赖 `lock` 方法返回的 `LockResult<MutexGuard<T>>``MutexGuard<T>` 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 `Mutex` 守护的资源,不过如果没有认真的思考 `MutexGuard<T>` 的生命周期的话,也可能会导致比预期更久的持有锁。因为 `while` 表达式中的值在整个块一直处于作用域中,`job.call_box()` 调用的过程中其仍然持有锁,这意味着其他 worker 不能接收任务。
相反通过使用 `loop` 并在循环块之内而不是之外获取锁和任务,`lock` 方法返回的 `MutexGuard``let job` 语句结束之后立刻就被丢弃了。这确保了 `recv` 调用过程中持有锁,而在 `job.call_box()` 调用前锁就被释放了,这就允许并发处理多个请求了。

View File

@ -1,8 +1,8 @@
## 优雅停机与清理
> [ch20-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-03-graceful-shutdown-and-cleanup.md)
> [ch20-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/master/src/ch20-03-graceful-shutdown-and-cleanup.md)
> <br>
> commit 1f0136399ba2f5540ecc301fab04bd36492e5554
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
示例 20-21 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 <span class="keystroke">ctrl-C</span> 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。
@ -14,7 +14,7 @@
<span class="filename">文件名: src/lib.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
@ -223,7 +223,7 @@ impl Drop for ThreadPool {
<span class="caption">示例 20-25在对每个 worker 线程调用 `join` 之前向 worker 发送 `Message::Terminate`</span>
现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从通道收到终止消息的 worker。
现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从通道收到终止消息的 worker。
为了更好的理解为什么需要两个分开的循环,想象一下只有两个 worker 的场景。如果在一个单独的循环中遍历每个 worker在第一次迭代中向通道发出终止消息并对第一个 worker 线程调用 `join`。我们会一直等待第一个 worker 结束,不过它永远也不会结束因为第二个线程接收了终止消息。死锁!
@ -282,7 +282,9 @@ Shutting down worker 3
这个特定的运行过程中一个有趣的地方在于:注意我们向通道中发出终止消息,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有收到终止消息,所以主线程阻塞直到 worker 0 结束。与此同时,每一个线程都收到了终止消息。一旦 worker 0 结束,主线程就等待其他 worker 结束,此时他们都已经收到终止消息并能够停止了。
恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。如下是完整的代码参考:
恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。
如下是完整的代码参考:
<span class="filename">文件名: src/bin/main.rs</span>
@ -368,16 +370,16 @@ impl<F: FnOnce()> FnBox for F {
}
}
type Job = Box<FnBox + Send + 'static>;
type Job = Box<dyn FnBox + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
/// 创建线程池。
///
/// The size is the number of threads in the pool.
/// 线程池中线程的数量。
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
/// `new` 函数在 size 为 0 时会 panic。
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
@ -469,8 +471,8 @@ impl Worker {
- 为库的功能增加测试
- 将 `unwrap` 调用改为更健壮的错误处理
- 使用 `ThreadPool` 进行其他不同于处理网络请求的任务
- 在 crates.io 寻找一个线程池 crate 并使用它实现一个类似的 web server将其 API 和鲁棒性与我们的实现做对比
- 在 *https://crates.io/* 寻找一个线程池 crate 并使用它实现一个类似的 web server将其 API 和鲁棒性与我们的实现做对比
## 总结
好极了!你结束了本书的学习!由衷感谢你我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。
好极了!你结束了本书的学习!由衷感谢你我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。