check to ch13-04

This commit is contained in:
KaiserY 2018-12-06 23:31:27 +08:00
parent 1b6e2fee59
commit d11ba9796e
5 changed files with 43 additions and 41 deletions

View File

@ -1,16 +1,18 @@
# Rust 中的函数式语言功能:迭代器与闭包
> [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-00-functional-features.md)
> [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/src/ch13-00-functional-features.md)
> <br>
> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程***functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。我们不会在这里讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。
Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程***functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。
本章我们不会讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。
更具体的,我们将要涉及:
* **闭包***Closures*),一个可以储存在变量里的类似函数的结构
* **迭代器***Iterators*),一种处理元素序列的方式
* 如何使用这些功能来改进第十二章的 I/O 项目。
* 这两个功能的性能。(**剧透高能** 他们的速度超乎你的想象!)
* 这两个功能的性能。(**剧透警告** 他们的速度超乎你的想象!)
还有其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。

View File

@ -1,8 +1,8 @@
## 闭包:可以捕获环境的匿名函数
> [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md)
> [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/src/ch13-01-closures.md)
> <br>
> commit bdcba470e4a71d16242b1727bbf99b300c194c46
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
Rust 的 **闭包***closures*)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。
@ -27,14 +27,14 @@ fn simulated_expensive_calculation(intensity: u32) -> u32 {
}
```
<span class="caption">示例 13-1一个用来代替假计算的函数,它大约会执行两秒钟</span>
<span class="caption">示例 13-1一个用来代替假计算的函数,它大约会执行两秒钟</span>
接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。
所需的输入有这些:
* **一个来自用户的 intensity 数字**,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。
* **一个随机数**,其会在健身计划中生成变化。
* 一个来自用户的 intensity 数字,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。
* 一个随机数,其会在健身计划中生成变化。
程序的输出将会是建议的锻炼计划。示例 13-2 展示了我们将要使用的 `main` 函数:
@ -178,7 +178,7 @@ let expensive_closure = |num| {
闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始,在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`
参数之后是存放闭包体的大括号 ———— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。
参数之后是存放闭包体的大括号 —— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。
注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。
@ -223,7 +223,7 @@ fn generate_workout(intensity: u32, random_number: u32) {
现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行改代码。
然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。
然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。
### 闭包类型推断和注解
@ -265,7 +265,7 @@ let add_one_v4 = |x| x + 1 ;
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
@ -446,7 +446,7 @@ fn generate_workout(intensity: u32, random_number: u32) {
第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败:
```rust,ignore
```rust,ignore,panics
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
@ -472,7 +472,7 @@ thread 'call_with_different_values' panicked at 'assertion failed: `(left == rig
尝试修改 `Cacher` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 会在哈希 map 中寻找 `arg`,如果存在就返回它。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。
当前 `Cacher` 实现的另一个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。
当前 `Cacher` 实现的第二个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。
### 闭包会捕获其环境
@ -502,7 +502,7 @@ fn main() {
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
fn main() {
let x = 4;
@ -543,7 +543,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
<span class="filename">文件名: src/main.rs</span>
```rust,ignore
```rust,ignore,does_not_compile
fn main() {
let x = vec![1, 2, 3];

View File

@ -1,8 +1,8 @@
## 使用迭代器处理元素序列
> [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-02-iterators.md)
> [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/src/ch13-02-iterators.md)
> <br>
> commit ceb31210263d49994bbf09456a35a135da690f24
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
迭代器模式允许你对一个项的序列进行某些处理。**迭代器***iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。
@ -16,7 +16,7 @@ let v1_iter = v1.iter();
<span class="caption">示例 13-13创建一个迭代器</span>
创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,不过直到现在我们掩盖了 `iter` 调用做了什么。
一旦创建迭代器之后,可以选择用多种方式利用它。在示例 3-4 中,我们使用迭代器和 `for` 循环在每一个项上执行了一些代码,虽然直到现在为止我们一直掩盖了 `iter` 调用做了什么。
示例 13-14 中的例子将迭代器的创建和 `for` 循环中的使用分开。迭代器被储存在 `v1_iter` 变量中,而这时没有进行迭代。一旦 `for` 循环开始使用 `v1_iter`,接着迭代器中的每一个元素被用于循环的一次迭代,这会打印出其每一个值:
@ -34,7 +34,7 @@ for val in v1_iter {
在标准库中没有提供迭代器的语言中,我们可能会使用一个从 0 开始的索引变量,使用这个变量索引 vector 中的值,并循环增加其值直到达到 vector 的元素数量。
迭代器为我们处理了所有这些逻辑,这减少了重复代码并潜在的消除了混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。
迭代器为我们处理了所有这些逻辑,这减少了重复代码并消除了潜在的混乱。另外,迭代器的实现方式提供了对多种不同的序列使用相同逻辑的灵活性,而不仅仅是像 vector 这样可索引的数据结构.让我们看看迭代器是如何做到这些的。
### `Iterator` trait 和 `next` 方法
@ -46,17 +46,19 @@ trait Iterator {
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
// 此处省略了方法的默认实现
}
```
注意这里有一下我们还未讲到的新语法:`type Item` 和 `Self::Item`,他们定义了 trait 的 **关联类型***associated type*)。第十九章会深入讲解关联类型,不过现在只需知道这段代码表明实现 `Iterator` trait 要求同时定义一个 `Item` 类型,这个 `Item` 类型被用作 `next` 方法的返回值类型。换句话说,`Item` 类型将是迭代器返回元素的类型。
`next``Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`。如果你希望的话可以直接调用迭代器的 `next` 方法;示例 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值:
`next``Iterator` 实现者被要求定义的唯一方法。`next` 一次返回迭代器中的一个项,封装在 `Some` 中,当迭代器结束时,它返回 `None`
可以直接调用迭代器的 `next` 方法;示例 13-15 有一个测试展示了重复调用由 vector 创建的迭代器的 `next` 方法所得到的值:
<span class="filename">文件名: src/lib.rs</span>
```rust,test_harness
```rust
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
@ -109,7 +111,7 @@ fn iterator_sum() {
<span class="filename">文件名: src/main.rs</span>
```rust
```rust,not_desired_behavior
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
@ -134,7 +136,7 @@ and do nothing unless consumed
为了修复这个警告并消费迭代器获取有用的结果,我们将使用第十二章简要讲到的 `collect` 方法。这个方法消费迭代器并将结果收集到一个数据结构中。
在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加的结果:
在示例 13-18 中,我们将遍历由 `map` 调用生成的迭代器的结果收集到一个 vector 中,它将会含有原始 vector 中每个元素加 1 的结果:
<span class="filename">文件名: src/main.rs</span>
@ -158,7 +160,7 @@ assert_eq!(v2, vec![2, 3, 4]);
<span class="filename">文件名: src/lib.rs</span>
```rust,test_harness
```rust
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
@ -209,7 +211,6 @@ fn filters_by_size() {
示例 13-20 有一个 `Counter` 结构体定义和一个创建 `Counter` 实例的关联函数 `new`
<span class="filename">文件名: src/lib.rs</span>
```rust
@ -350,4 +351,4 @@ fn using_other_iterator_trait_methods() {
注意 `zip` 只产生四对值;理论上第五对值 `(5, None)` 从未被产生,因为 `zip` 在任一输入迭代器返回 `None` 时也返回 `None`
所有这些方法调用都是可能的,因为我们通过指定 `next` 如何工作来实现 `Iterator` trait 而标准库则提供其他调用 `next` 的默认方法实现。
所有这些方法调用都是可能的,因为我们指定了 `next` 方法如何工作,而标准库则提供了其它调用 `next` 的方法的默认实现。

View File

@ -1,14 +1,14 @@
## 改进 I/O 项目
> [ch13-03-improving-our-io-project.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-03-improving-our-io-project.md)
> [ch13-03-improving-our-io-project.md](https://github.com/rust-lang/book/blob/master/src/ch13-03-improving-our-io-project.md)
> <br>
> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
有了这些关于迭代器的新知识,我们可以使用迭代器来改进第十二章中 I/O 项目的实现来使得代码更简洁明了。让我们看看迭代器如何能够改进 `Config::new` 函数和 `search` 函数的实现。
### 使用迭代器并去掉 `clone`
在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中原原本本的重现了第十二章结尾示例 12-23 中 `Config::new` 函数的实现:
在示例 12-6 中,我们增加了一些代码获取一个 `String` slice 并创建一个 `Config` 结构体的实例,他们索引 slice 中的值并克隆这些值以便 `Config` 结构体可以拥有这些值。在示例 13-24 中重现了第十二章结尾示例 12-23 中 `Config::new` 函数的实现:
<span class="filename">文件名: src/lib.rs</span>
@ -58,9 +58,7 @@ fn main() {
}
```
我们会修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。直到同时更新 `Config::new` 这些代码还不能编译:
将他们改为如示例 13-25 所示:
我们会修改第十二章结尾示例 12-24 中的 `main` 函数的开头为示例 13-25 中的代码。直到同步更新 `Config::new` 之前这些代码还不能编译:
<span class="filename">文件名: src/main.rs</span>
@ -100,6 +98,7 @@ impl Config {
<span class="filename">文件名: src/lib.rs</span>
```rust
# fn main() {}
# use std::env;
#
# struct Config {
@ -129,13 +128,13 @@ impl Config {
}
```
<span class="caption">示例 13-27修改 `Config::new` 的函数体使用迭代器方法</span>
<span class="caption">示例 13-27修改 `Config::new` 的函数体使用迭代器方法</span>
请记住 `env::args` 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 `next` 并不对返回值做任何操作。之后对希望放入 `Config` 中字段 `query` 调用 `next`。如果 `next` 返回 `Some`,使用 `match` 来提取其值。如果它返回 `None`,则意味着没有提供足够的参数并通过 `Err` 值提早返回。对 `filename` 值进行同样的操作。
### 使用迭代器适配器来使代码更简明
I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义:
I/O 项目中其他可以利用迭代器优势的地方位于 `search` 函数,示例 13-28 中重现了第十二章结尾示例 12-19 中此函数的定义:
<span class="filename">文件名: src/lib.rs</span>
@ -153,9 +152,9 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
}
```
<span class="caption">示例 13-28第十二章结尾 `search` 函数的定义</span>
<span class="caption">示例 13-28示例 12-19 中 `search` 函数的定义</span>
可以通过使用迭代器适配器方法来编写更的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。示例 13-29 展示了该变化:
可以通过使用迭代器适配器方法来编写更简明的代码。这也避免了一个可变的中间 `results` vector 的使用。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去掉可变状态可能会使得将来进行并行搜索的增强变得更容易,因为我们不必管理 `results` vector 的并发访问。示例 13-29 展示了该变化:
<span class="filename">文件名: src/lib.rs</span>
@ -169,7 +168,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
<span class="caption">示例 13-29`search` 函数实现中使用迭代器适配器</span>
回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 为真的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。
回忆 `search` 函数的目的是返回所有 `contents` 中包含 `query` 的行。类似于示例 13-19 中的 `filter` 例子,可以使用 `filter` 适配器只保留 `line.contains(query)` 返回 `true` 的那些行。接着使用 `collect` 将匹配行收集到另一个 vector 中。这样就容易多了!请随意对 `search_case_insensitive` 函数做出同样的使用迭代器方法的修改。
接下来的逻辑问题就是在代码中应该选择哪种风格:示例 13-28 中的原始实现,或者是示例 13-29 中使用迭代器的版本。大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以理解,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器可能会更容易理解。相比摆弄不同的循环并创建新 vector迭代器代码则更关注循环的目的。这抽象出了那些老生常谈的代码这样就更容易看清代码所特有的概念比如迭代器中每个元素必须面对的过滤条件。

View File

@ -1,8 +1,8 @@
## 性能对比:循环 VS 迭代器
> [ch13-04-performance.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-04-performance.md)
> [ch13-04-performance.md](https://github.com/rust-lang/book/blob/master/src/ch13-04-performance.md)
> <br>
> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
为了决定使用哪个实现,我们需要知道哪个版本的 `search` 函数更快:直接使用 `for` 循环的版本还是使用迭代器的版本。