From 06901f5ac7c5dcb137f6edcb97a8f3cae2b848b1 Mon Sep 17 00:00:00 2001 From: KaiserY Date: Tue, 29 Aug 2017 18:40:55 +0800 Subject: [PATCH] check ch13-02 --- src/ch13-01-closures.md | 523 +++++++++++++++++++++++++++++++++------ src/ch13-02-iterators.md | 191 +------------- 2 files changed, 452 insertions(+), 262 deletions(-) diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index a37b0d4..0789656 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -4,113 +4,461 @@ >
> commit 56352c28cf3fe0402fa5a7cba73890e314d720eb -Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。 +Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在以不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。 +### 使用闭包创建行为的抽象 +让我们看看一个展示储存闭包并在之后执行如何有用的情形的例子。其间我们会讨论闭包的语法、类型推断和 trait。 -Rust 提供了定义**闭包**的能力,它类似于函数。让我们先不从技术上的定义开始,而是看看闭包语句结构,然后再返回他们的定义。列表 13-1 展示了一个被赋值给变量`add_one`的小的闭包定义,之后可以用这个变量来调用闭包: +这个假想的情况如下:我们在一个通过 app 生成自定义健身计划的初创企业工作。其后端使用 Rust 编写,而生成健身计划的算法需要考虑很多不同的因素,比如用户的年龄、身体质量指数(Body Mass Index)、用户喜好、最近的健身活动和用户指定的强度系数。本例中实际的算法并不重要。我们只希望在需要时调用算法,并且只希望调用一次,这样就不会让用户等得太久。这里将通过调用 `simulated_expensive_calculation` 函数来模拟调用假象的算法,如列表 13-1 所示,它会打印出 `calculating slowly...`,等待两秒,并接着返回传递给它的数字: -Filename: src/main.rs +文件名: src/main.rs ```rust -fn main() { - let add_one = |x| x + 1; +use std::thread; +use std::time::Duration; - let five = add_one(4); - - assert_eq!(5, five); +fn simulated_expensive_calculation(intensity: i32) -> i32 { + println!("calculating slowly..."); + thread::sleep(Duration::from_secs(2)); + intensity } ``` -Listing 13-1: A closure that takes one parameter and adds -one to it, assigned to the variable `add_one` +列表 13-1:一个用来代替假象计算的函数,它大约会执行两秒 -闭包的定义位于第一行,展示了闭包获取了一个叫做`x`的参数。闭包的参数位于竖线之间(`|`)。 +接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。 -这是一个很小的闭包,它只包含一个表达式。列表 13-2 展示了一个稍微复杂一点的闭包: +程序的输入有: -Filename: src/main.rs +- 一个来自用户的 `intensity` 数字,请求健身计划时指定,它代表用户喜好低强度还是高强度健身 +- 一个会在健身计划中生成变化的随机数 + +程序的输出将会是建议的锻炼计划。 + +列表 13-2 展示了我们将要使用的 `main` 函数。处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: + +文件名: src/main.rs ```rust fn main() { - let calculate = |a, b| { - let mut result = a * 2; + let simulated_user_specified_value = 10; + let simulated_random_number = 7; - result += b; + generate_workout(simulated_user_specified_value, simulated_random_number); +} +# fn generate_workout(intensity: i32, random_number: i32) {} +``` - result +列表 13-2:`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入 + +这就是我们的执行上下文。列表 13-3 中的 `generate_workout` 函数包含我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中: + +文件名: src/main.rs + +```rust +# use std::thread; +# use std::time::Duration; +# +# fn simulated_expensive_calculation(num: i32) -> i32 { +# println!("calculating slowly..."); +# thread::sleep(Duration::from_secs(2)); +# num +# } +# +fn generate_workout(intensity: i32, random_number: i32) { + if intensity < 25 { + println!( + "Today, do {} pushups!", + simulated_expensive_calculation(intensity) + ); + println!( + "Next, do {} situps!", + simulated_expensive_calculation(intensity) + ); + } else { + if random_number == 3 { + println!("Take a break today! Remember to stay hydrated!"); + } else { + println!( + "Today, run for {} minutes!", + simulated_expensive_calculation(intensity) + ) + } + } +} +``` + +列表 13-3:程序的业务逻辑,它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划 + +列表 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,`else` 中的 `else` 调用了它一次。 + +`generate_workout` 函数的合意的行为是首先检查用户需要低强度(由小于 25 的系数代表)锻炼还是高强度(25 或以上)锻炼。低强度锻炼计划会根据由 `simulated_expensive_calculation` 函数所模拟的复杂算法建议一定数量的俯卧撑和仰卧起坐,此函数需要强度系数作为输入。 + +如果用户需要高强度锻炼,这里有一些额外的逻辑:如果 app 生成的随机数刚好是 3,app 相反会建议用户稍做休息并补充水分。如果不是,则用户会从复杂算法中得到数分钟跑步的高强度锻炼计划。 + +数据科学部门的同学告知我们必须对调用算法的方式做出一些改变。为了简化做出这些改变的更新,我们将重构代码来只调用 `simulated_expensive_calculation` 一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过最终仍然需要调用函数一次。 + +有多种方法可以重构此程序。我们首先尝试的是将重复的慢计算函数调用提取到一个变量中,如列表 13-4 所示: + +文件名: src/main.rs + +```rust +# use std::thread; +# use std::time::Duration; +# +# fn simulated_expensive_calculation(num: i32) -> i32 { +# println!("calculating slowly..."); +# thread::sleep(Duration::from_secs(2)); +# num +# } +# +fn generate_workout(intensity: i32, random_number: i32) { + let expensive_result = + simulated_expensive_calculation(intensity); + + if intensity < 25 { + println!( + "Today, do {} pushups!", + expensive_result + ); + println!( + "Next, do {} situps!", + expensive_result + ); + } else { + if random_number == 3 { + println!("Take a break today! Remember to stay hydrated!"); + } else { + println!( + "Today, run for {} minutes!", + expensive_result + ) + } + } +} +``` + +列表 13-4:将 `simulated_expensive_calculation` 调用提取到一个位置,位于 `if` 块之前并将结果储存在变量 `expensive_result` 中 + +这个修改统一了 `simulated_expensive_calculation` 调用并解决了第一个 `if` 块中不必要的两次调用函数的问题。不幸的是,现在所有的情况下都需要调用函数并等待结果,而内部 `if` 块完全不需要其结果。 + +我们希望能够在程序的一个位置指定某些代码,并只在程序的某处实际需要结果的时候执行这些代码。这正是闭包的用武之地! + +### 闭包储存了之后会执行的代码 + +不同于总是在 `if` 块之前调用 `simulated_expensive_calculation` 函数并储存其结果,我们可以定义一个闭包并将其储存在变量中,如列表 13-5 所示。实际上可以选择将整个 `simulated_expensive_calculation` 函数体移动到这里引入的闭包中: + +文件名: src/main.rs + +```rust +# use std::thread; +# use std::time::Duration; +# +let expensive_closure = |num| { + println!("calculating slowly..."); + thread::sleep(Duration::from_secs(2)); + num +}; +# expensive_closure(5); +``` + +列表 13-5:使用慢计算的函数体定义一个闭包并储存到变量 `expensive_closure` 中 + +闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始。在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`。 + +参数之后是存放闭包体的大括号。如果闭包体只有一行则大括号是可以省略的。大括号之后,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号,正如函数体中的一样。 + +注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。 + +现在我们定义了闭包,可以改变 `if` 块中的代码来调用闭包以执行代码并获取结果值。调用闭包看起来非常类似调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号,如列表 13-6 所示: + +文件名: src/main.rs + +```rust +# use std::thread; +# use std::time::Duration; +# +fn generate_workout(intensity: i32, random_number: i32) { + let expensive_closure = |num| { + println!("calculating slowly..."); + thread::sleep(Duration::from_secs(2)); + num }; - assert_eq!(7, calculate(2, 3)); // 2 * 2 + 3 == 7 - assert_eq!(13, calculate(4, 5)); // 4 * 2 + 5 == 13 + if intensity < 25 { + println!( + "Today, do {} pushups!", + expensive_closure(intensity) + ); + println!( + "Next, do {} situps!", + expensive_closure(intensity) + ); + } else { + if random_number == 3 { + println!("Take a break today! Remember to stay hydrated!"); + } else { + println!( + "Today, run for {} minutes!", + expensive_closure(intensity) + ) + } + } } ``` -Listing 13-2: A closure with two parameters and multiple -expressions in its body +列表 13-6:调用定义的 `expensive_closure` -可以通过大括号来定义多于一个表达式的闭包体。 +现在我们达成了将满计算统一到一个地方的目标,并只会在需要结果的时候执行改代码。然而,我们又重新引入了列表 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 -你会注意到一些闭包不同于`fn`关键字定义的函数的地方。第一个不同是并不需要声明闭包的参数和返回值的类型。也可以选择加上类型注解;列表 13-3 展示了列表 13-1 中闭包带有参数和返回值类型注解的版本: +### 闭包类型推断和注解 +闭包与由 `fn` 函数定义的函数有一些区别。第一是不要求像 `fn` 函数那样在参数和返回值上注明类型。 -Filename: src/main.rs +函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。 + +另外,闭包通常很短并只与对应相对任意的场景较小的上下文中。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。 + +类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解;为列表 13-4 中定义的闭包标注类型将看起来像列表 13-7 中的定义: + +文件名: src/main.rs ```rust -fn main() { - let add_one = |x: i32| -> i32 { x + 1 }; - - assert_eq!(2, add_one(1)); -} +# use std::thread; +# use std::time::Duration; +# +let expensive_closure = |num: i32| -> i32 { + println!("calculating slowly..."); + thread::sleep(Duration::from_secs(2)); + num +}; ``` -Listing 13-3: A closure definition with optional -parameter and return value type annotations +列表 13-7:为闭包的参数和返回值增加可选的类型注解 -在带有类型注解的情况下闭包的语法于函数就更接近了。让我们来更直接的比较一下不同闭包的语法与函数的语法。这里增加了一些空格来对齐相关的部分: +有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对其相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法: ```rust,ignore -fn add_one_v1 (x: i32) -> i32 { x + 1 } // a function -let add_one_v2 = |x: i32| -> i32 { x + 1 }; // the full syntax for a closure -let add_one_v3 = |x| { x + 1 }; // a closure eliding types -let add_one_v4 = |x| x + 1 ; // without braces +fn add_one_v1 (x: i32) -> i32 { x + 1 } +let add_one_v2 = |x: i32| -> i32 { x + 1 }; +let add_one_v3 = |x| { x + 1 }; +let add_one_v4 = |x| x + 1 ; ``` -定义闭包时不要求类型注解而在定义函数时要求的原因在于函数是显式暴露给用户的接口的一部分,所以为了严格的定义接口确保所有人都同意函数使用和返回的值类型是很重要的。但是闭包并不像函数那样用于暴露接口:他们存在于绑定中并直接被调用。强制标注类型就等于为了很小的优点而显著的降低了工程性(本末倒置)。 +第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。 -不过闭包的定义确实会推断每一个参数和返回值的类型。例如,如果用`i8`调用列表 13-1 中没有类型注解的闭包,如果接着用`i32`调用同一闭包则会得到一个错误: +闭包定义会为每个参数和返回值推断一个具体类型。例如,列表 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第一次使用 `i32`,则会得到一个错误: -Filename: src/main.rs +文件名: src/main.rs ```rust,ignore -let add_one = |x| x + 1; +let example_closure = |x| x; -let five = add_one(4i8); -assert_eq!(5i8, five); - -let three = add_one(2i32); +let s = example_closure(String::from("hello")); +let n = example_closure(5); ``` +列表 13-8:尝试调用一个被推断为两个不同类型的闭包 + 编译器给出如下错误: -``` +```text error[E0308]: mismatched types - --> + --> src/main.rs | -7 | let three = add_one(2i32); - | ^^^^ expected i8, found i32 + | let n = example_closure(5); + | ^ expected struct `std::string::String`, found + integral variable + | + = note: expected type `std::string::String` + found type `{integer}` ``` -因为闭包是直接被调用的所以能可靠的推断出其类型,再强制要求标注类型就显得有些冗余了。 +第一次使用 `String` 值调用 `example_closure` 时,编译器推断 `x` 和此闭包返回值的类型为 `String`。接着这些类型被锁定进闭包 `example_closure` 中,如果尝试对同一闭包使用不同类型则会得到类型错误。 -闭包与函数语法不同还有另一个原因是,它与函数有着不同的行为:闭包拥有其**环境(上下文)**。 +### 使用带有泛型和 `Fn` trait 的闭包 -### 闭包可以引用其环境 +回到我们的健身计划生成 app ,在列表 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。 -我们知道函数只能使用其作用域内的变量,或者要么是`const`的要么是被声明为参数的。闭包则可以做的更多:闭包允许使用包含他们的作用域的变量。列表 13-4 是一个在`equal_to_x`变量中并使用其周围环境中变量`x`的闭包的例子: +然而,因为拥有一个慢计算的闭包,我们还可以采取另一个解决方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation*。 +为了让结构体存放闭包,我们需要能够指定闭包的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 -Filename: src/main.rs +`Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn`、`FnMut` 或 `FnOnce` 中的一个。在下一部分捕获环境部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。 + +为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `i32` 的参数并返回一个 `i32`,这样所指定的 trait bound 就是 `Fn(i32) -> i32`。 + +列表 13-9 展示了存放了闭包和一个 Option 结果值的 `Cacher` 结构体的定义: + +文件名: src/main.rs + +```rust +struct Cacher + where T: Fn(i32) -> i32 +{ + calculation: T, + value: Option, +} +``` + +列表 13-9:定义一个 `Cacher` 结构体来在 `calculation` 中存放闭包并在 `value` 中存放 Option 值 + +结果提 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `i32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `i32`(由 `->` 之后的内容)。 + +`value` 是 `Option` 类型的。在执行闭包之前,`value` 将是 `None`。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。 + +刚才讨论的油管 `value` 字段逻辑定义于列表 13-10: + +文件名: src/main.rs + +```rust +# struct Cacher +# where T: Fn(i32) -> i32 +# { +# calculation: T, +# value: Option, +# } +# +impl Cacher + where T: Fn(i32) -> i32 +{ + fn new(calculation: T) -> Cacher { + Cacher { + calculation, + value: None, + } + } + + fn value(&mut self, arg: i32) -> i32 { + match self.value { + Some(v) => v, + None => { + let v = (self.calculation)(arg); + self.value = Some(v); + v + }, + } + } +} +``` + +列表 13-10:一个 `Cacher` 的关联函数 `new` 和管理缓存逻辑的 `value` 方法的实现 + +`Cacher` 结构体的字段是私有的,因为我们希望 `Cacher` 管理这些值而不是任由调用代码潜在的直接改变他们。`Cacher::new` 函数获取一个泛型参数 `T`,它定义于 `impl` 块上下文中并与 `Cacher` 结构体有着相同的 trait bound。`Cacher::new` 返回一个在 `calculation` 字段中存放了指定闭包和在 `value` 字段中存放了 `None` 值的 `Cacher` 实例,因为我们还未执行闭包。 + +当调用代码需要闭包的执行结果时,不同于直接调用闭包,它会调用 `value` 方法。这个方法会检查 `self.value` 是否已经有了一个 `Some` 的结果值;如果有,它返回 `Some` 中的值并不会再次执行闭包。 + +如果 `self.value` 是 `None`,则会调用 `self.calculation` 中储存的闭包,将结果保存到 `self.value` 以便将来使用,并同时返回结果值。 + +列表 13-11 展示了如何在列表 13-6 的 `generate_workout` 函数中利用 `Cacher` 结构体: + +文件名: src/main.rs + +```rust +# use std::thread; +# use std::time::Duration; +# +# struct Cacher +# where T: Fn(i32) -> i32 +# { +# calculation: T, +# value: Option, +# } +# +# impl Cacher +# where T: Fn(i32) -> i32 +# { +# fn new(calculation: T) -> Cacher { +# Cacher { +# calculation, +# value: None, +# } +# } +# +# fn value(&mut self, arg: i32) -> i32 { +# match self.value { +# Some(v) => v, +# None => { +# let v = (self.calculation)(arg); +# self.value = Some(v); +# v +# }, +# } +# } +# } +# +fn generate_workout(intensity: i32, random_number: i32) { + let mut expensive_result = Cacher::new(|num| { + println!("calculating slowly..."); + thread::sleep(Duration::from_secs(2)); + num + }); + + if intensity < 25 { + println!( + "Today, do {} pushups!", + expensive_result.value(intensity) + ); + println!( + "Next, do {} situps!", + expensive_result.value(intensity) + ); + } else { + if random_number == 3 { + println!("Take a break today! Remember to stay hydrated!"); + } else { + println!( + "Today, run for {} minutes!", + expensive_result.value(intensity) + ) + } + } +} +``` + +列表 13-11:在 `generate_workout` 函数中利用 `Cacher` 结构体来抽象出缓存逻辑 + +不同于直接将闭包保存进一个变量,我们保存一个新的 `Cacher` 实例来存放闭包。接着,在每一个需要结果的地方,调用 `Cacher` 实例的 `value` 方法。可以调用 `value` 方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。尝试使用列表 13-2 中的 `main` 函数来运行这段程序,并将 `simulated_user_specified_value` 和 `simulated_random_number` 变量中的值来验证在所有情况下在多个 `if` 和 `else` 块中,闭包打印的 `calculating slowly...` 只会在需要时出现并只会出现一次。 + +`Cacher` 负责确保不会调用超过所需的慢计算所需的逻辑,这样 `generate_workout` 就可以专注业务逻辑了。值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。 + +第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败: + +```rust,ignore +#[test] +fn call_with_different_values() { + let mut c = Cacher::new(|a| a); + + let v1 = c.value(1); + let v2 = c.value(2); + + assert_eq!(v2, 2); +} +``` + +这个测试使用返回传递给它的值的闭包创建了一个新的 `Cacher` 实例。使用为 1 的 `arg` 和为 2 的 `arg` 调用 `Cacher` 实例的 `value` 方法,同时我们期望使用为 2 的 `arg` 调用 `value` 会返回 2。 + +使用列表 13-9 和列表 13-10 的 `Cacher` 实现运行测试,它会在 `assert_eq!` 失败并显示如下信息: + +```text +thread 'call_with_different_arg_values' panicked at 'assertion failed: +`(left == right)` (left: `1`, right: `2`)', src/main.rs +``` + +这里的问题是第一次使用 1 调用 `c.value`,`Cacher` 实例将 `Some(1)` 保存进 `self.value`。在这之后,无论传递什么值调用 `value`,它总是会返回 1。 + +尝试修改 `Cacher` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 是 `Some` 还是 `None` 值,现在 `value` 会在哈希 map 中寻找 `arg`,如果存在就返回它。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。 + +当前 `Cacher` 实现的另一个问题是它的应用被限制为只接受获取一个 `i32` 值并返回一个 `i32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 + +### 闭包会捕获其环境 + +在健身计划生成器的例子中,我们只将闭包作为内联匿名函数来使用。不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问定义他们的作用域的变量。 + +列表 13-12 有一个储存在 `equal_to_x` 变量中闭包的例子,它使用了闭包环境中的变量 `x`: + +文件名: src/main.rs ```rust fn main() { @@ -124,12 +472,13 @@ fn main() { } ``` -Listing 13-4: Example of a closure that refers to a -variable in its enclosing scope +列表 13-12:一个引用了其周围作用域中变量的闭包示例 -这里。即便`x`并不是`equal_to_x`的一个参数,`equal_to_x`闭包也被允许使用它,因为变量`x`定义于同样定义`equal_to_x`的作用域中。并不允许在函数中进行与列表 13-4 相同的操作;尝试这么做看看会发生什么: +这里,即便 `x` 并不是 `equal_to_x` 的一个参数,`equal_to_x` 闭包也被允许使用变量 `x`,因为它与 `equal_to_x` 定义于相同的作用域。 -Filename: src/main.rs +函数则不能做到同样的事,让我们试试会发生什么: + +文件名: src/main.rs ```rust,ignore fn main() { @@ -143,9 +492,9 @@ fn main() { } ``` -我们会得到一个错误: +这会得到一个错误: -``` +```text error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } closure form instead --> @@ -154,28 +503,54 @@ closure form instead | ^ ``` -编译器甚至提醒我们这只能用于闭包! +编译器甚至会提示我们这只能用于闭包! -获取他们环境中值的闭包主要用于开始新线程的场景。我们也可以定义以闭包作为参数的函数,通过使用`Fn` trait。这里是一个函数`call_with_one`的例子,它的签名有一个闭包参数: +当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,当执行不会捕获环境的更通用的代码场景中我们不希望有这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。 -```rust -fn call_with_one(some_closure: F) -> i32 - where F: Fn(i32) -> i32 { +闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,不可变借用和可变借用。这三种捕获值的方式被编码为如下三个 `Fn` trait: - some_closure(1) +* `FnOnce` 消费从周围作用域捕获的变量(闭包周围的作用域被称为其 **环境**,*environment*)。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 `Once` 部分是因为闭包不能多次获取相同变量的所有权,所以它只能被调用一次。 +* `Fn` 从其环境不可变的借用值 +* `FnMut` 可变的借用值所以可以改变其环境 + +当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在列表 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait),因为闭包体只需要读取 `x` 的值。 + +如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了列表 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动: + +文件名: src/main.rs + +```rust,ignore +fn main() { + let x = vec![1, 2, 3]; + + let equal_to_x = move |z| z == x; + + println!("can't use x here: {:?}", x); + + let y = vec![1, 2, 3]; + + assert!(equal_to_x(y)); } - -let answer = call_with_one(|x| x + 2); - -assert_eq!(3, answer); ``` -我们将`|x| x + 2`传递给了`call_with_one`,而`call_with_one`用`1`作为参数调用了这个闭包。`some_closure`调用的返回值接着被`call_with_one`返回。 +这个例子并不能编译: -`call_with_one`的签名使用了第十章 trait 部分讨论到的`where`语法。`some_closure`参数有一个泛型类型`F`,它在`where`从句中被定义为拥有`Fn(i32) -> i32` trait bound。`Fn` trait 代表了一个闭包,而且可以给`Fn` trait 增加类型来代表一个特定类型的闭包。在这种情况下,闭包拥有一个`i32`的参数并返回一个`i32`,所以泛型的 trait bound 被指定为`Fn(i32) -> i32`。 +```text +error[E0382]: use of moved value: `x` + --> src/main.rs:6:40 + | +4 | let equal_to_x = move |z| z == x; + | -------- value moved (into closure) here +5 | +6 | println!("can't use x here: {:?}", x); + | ^ value used here after move + | + = note: move occurs because `x` has type `std::vec::Vec`, which does not + implement the `Copy` trait +``` -在函数签名中指定闭包要求使用泛型和 trait bound。每一个闭包都有一个独特的类型,所以不能写出闭包的类型而必须使用泛型。 +`x` 被移动进了闭包,因为闭包使用 `move` 关键字定义。接着闭包获取了 `x` 的所有权,同时 `main` 就不再允许使用它了。去掉 `println!` 即可修复问题。 -`Fn`并不是唯一可以指定闭包的 trait bound,事实上有三个:`Fn`、`FnMut`和`FnOnce`。这是在 Rust 中经常见到的三种模式的延续:借用、可变借用和获取所有权。用`Fn`来指定可能只会借用其环境中值的闭包。用`FnMut`来指定会修改环境中值的闭包,而如果闭包会获取环境值的所有权则使用`FnOnce`。大部分情况可以从`Fn`开始,而编译器会根据调用闭包时会发生什么来告诉你是否需要`FnMut`或`FnOnce`。 +大部分需要指定一个 `Fn` trait bound 的时候,可以从 `Fn` 开始,编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce`。 -为了展示拥有闭包作为参数的函数的应用场景,让我们继续下一主题:迭代器。 \ No newline at end of file +为了展示闭包作为函数参数时捕获其环境的作用,让我们移动到下一个主题:迭代器。 \ No newline at end of file diff --git a/src/ch13-02-iterators.md b/src/ch13-02-iterators.md index d810770..4c2a036 100644 --- a/src/ch13-02-iterators.md +++ b/src/ch13-02-iterators.md @@ -2,193 +2,8 @@ > [ch13-02-iterators.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-02-iterators.md) >
-> commit 431116f5c696000b9fd6780e5fde90392cef6812 +> commit b9459971e4bc6f37b4d18b38c6fe9221317fd985 -迭代器是 Rust 中的一个模式,它允许你对一个项的序列进行某些处理。例如。列表 13-5 中对 vecctor 中的每一个数加一: +迭代器模式允许你对一个项的序列进行某些处理。**迭代器**(*iterator*)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。 -```rust -let v1 = vec![1, 2, 3]; - -let v2: Vec = v1.iter().map(|x| x + 1).collect(); - -assert_eq!(v2, [2, 3, 4]); -``` - -Listing 13-5: Using an iterator, `map`, and `collect` to -add one to each number in a vector - - - -vector 的`iter`方法允许从 vector 创建一个**迭代器**(*iterator*)。接着迭代器上的`map`方法调用允许我们处理每一个元素:在这里,我们向`map`传递了一个对每一个元素`x`加一的闭包。`map`是最基本的与比较交互的方法之一,因为依次处理每一个元素是非常有用的!最后`collect`方法消费了迭代器并将其元素存放到一个新的数据结构中。在这个例子中,因为我们指定`v2`的类型是`Vec`,`collect`将会创建一个`i32`的 vector。 - -像`map`这样的迭代器方法有时被称为**迭代器适配器**(*iterator adaptors*),因为他们获取一个迭代器并产生一个新的迭代器。也就是说,`map`在之前迭代器的基础上通过调用传递给它的闭包来创建了一个新的值序列的迭代器。 - -概括一下,这行代码进行了如下工作: - -1. 从 vector 中创建了一个迭代器。 -2. 使用`map`适配器和一个闭包参数对每一个元素加一。 -3. 使用`collect`适配器来消费迭代器并生成了一个新的 vector。 - -这就是如何产生结果`[2, 3, 4]`的。如你所见,闭包是使用迭代器的很重要的一部分:他们提供了一个自定义类似`map`这样的迭代器适配器的行为的方法。 - -### 迭代器是惰性的 - -在上一部分,你可能已经注意到了一个微妙的用词区别:我们说`map`**适配**(*adapts*)了一个迭代器,而`collect`**消费**(*consumes*)了一个迭代器。这是有意为之的。单独的迭代器并不会做任何工作;他们是惰性的。也就是说,像列表 13-5 的代码但是不调用`collect`的话: - -```rust -let v1: Vec = vec![1, 2, 3]; - -v1.iter().map(|x| x + 1); // without collect -``` - -这可以编译,不过会给出一个警告: - -``` -warning: unused result which must be used: iterator adaptors are lazy and do -nothing unless consumed, #[warn(unused_must_use)] on by default - --> src/main.rs:4:1 - | -4 | v1.iter().map(|x| x + 1); // without collect - | ^^^^^^^^^^^^^^^^^^^^^^^^^ -``` - -这个警告是因为迭代器适配器实际上并不自己进行处理。他们需要一些其他方法来触发迭代器链的计算。我们称之为**消费适配器**(*consuming adaptors*),而`collect`就是其中之一。 - -那么如何知道迭代器方法是否消费了迭代器呢?还有哪些适配器是可用的呢?为此,让我们看看`Iterator` trait。 - -### `Iterator` trait - -迭代器都实现了一个标准库中叫做`Iterator`的 trait。其定义看起来像这样: - -```rust -trait Iterator { - type Item; - - fn next(&mut self) -> Option; -} -``` - -这里有一些还未讲到的新语法:`type Item`和`Self::Item`定义了这个 trait 的**关联类型**(*associated type*),第十九章会讲到关联类型。现在所有你需要知道就是这些代码表示`Iterator` trait 要求你也定义一个`Item`类型,而这个`Item`类型用作`next`方法的返回值。换句话说,`Item`类型将是迭代器返回的元素的类型。 - -让我们使用`Iterator` trait 来创建一个从一数到五的迭代器`Counter`。首先,需要创建一个结构体来存放迭代器的当前状态,它有一个`u32`的字段`count`。我们也定义了一个`new`方法,当然这并不是必须的。因为我们希望`Counter`能从一数到五,所以它总是从零开始: - -```rust -struct Counter { - count: u32, -} - -impl Counter { - fn new() -> Counter { - Counter { count: 0 } - } -} -``` - -接下来,我们将通过定义`next`方法来为`Counter`类型实现`Iterator` trait。我们希望迭代器的工作方式是对当前状态加一(这就是为什么将`count`初始化为零,这样迭代器首先就会返回一)。如果`count`仍然小于六,将返回当前状态,不过如果`count`大于等于六,迭代器将返回`None`,如列表 13-6 所示: - -```rust -# struct Counter { -# count: u32, -# } -# -impl Iterator for Counter { - // Our iterator will produce u32s - type Item = u32; - - fn next(&mut self) -> Option { - // increment our count. This is why we started at zero. - self.count += 1; - - // check to see if we've finished counting or not. - if self.count < 6 { - Some(self.count) - } else { - None - } - } -} -``` - -Listing 13-6: Implementing the `Iterator` trait on our -`Counter` struct - - - -`type Item = u32`这一行表明迭代器中`Item`的关联类型将是`u32`。同样无需担心关联类型,因为第XX章会涉及他们。 - -`next`方法是迭代器的主要接口,它返回一个`Option`。如果它是`Some(value)`,相当于可以迭代器中获取另一个值。如果它是`None`,迭代器就结束了。在`next`方法中可以进行任何迭代器需要的计算。在这个例子中,我们对当前状态加一,接着检查其是否仍然小于六。如果是,返回`Some(self.count)`来产生下一个值。如果大于等于六,迭代结束并返回`None`。 - -迭代器 trait 指定当其返回`None`,就代表迭代结束。该 trait 并不强制任何在`next`方法返回`None`后再次调用时必须有的行为。在这个情况下,在第一次返回`None`后每一次调用`next`仍然返回`None`,不过其内部`count`字段会依次增长到`u32`的最大值,接着`count`会溢出(在调试模式会`panic!`而在发布模式则会折叠从最小值开始)。有些其他的迭代器则选择再次从头开始迭代。如果需要确保迭代器在返回第一个`None`之后所有的`next`方法调用都返回`None`,可以使用`fuse`方法来创建不同于任何其他的迭代器。 - -一旦实现了`Iterator` trait,我们就有了一个迭代器!可以通过不停的调用`Counter`结构体的`next`方法来使用迭代器的功能: - -```rust,ignore -let mut counter = Counter::new(); - -let x = counter.next(); -println!("{:?}", x); - -let x = counter.next(); -println!("{:?}", x); - -let x = counter.next(); -println!("{:?}", x); - -let x = counter.next(); -println!("{:?}", x); - -let x = counter.next(); -println!("{:?}", x); - -let x = counter.next(); -println!("{:?}", x); -``` - -这会一次一行的打印出从`Some(1)`到`Some(5)`,之后就全是`None`。 - -### 各种`Iterator`适配器 - -在列表 13-5 中有一个迭代器并调用了其像`map`和`collect`这样的方法。然而在列表 13-6 中,只实现了`Counter`的`next`方法。`Counter`如何才能得到像`map`和`collect`这样的方法呢? - -好吧,当讲到`Iterator`的定义时,我们故意省略一个小的细节。`Iterator`定义了一系列默认实现,他们会调用`next`方法。因为`next`是唯一一个`Iterator` trait 没有默认实现的方法,一旦实现之后,`Iterator`的所有其他的适配器就都可用了。这些适配器可不少! - -例如,处于某种原因我们希望获取一个`Counter`实例产生的值,与另一个`Counter`实例忽略第一个值之后的值相组合,将每组数相乘,并只保留能被三整除的相乘结果,最后将所有保留的结果相加,我们可以这么做: - - -```rust -# struct Counter { -# count: u32, -# } -# -# impl Counter { -# fn new() -> Counter { -# Counter { count: 0 } -# } -# } -# -# impl Iterator for Counter { -# // Our iterator will produce u32s -# type Item = u32; -# -# fn next(&mut self) -> Option { -# // increment our count. This is why we started at zero. -# self.count += 1; -# -# // check to see if we've finished counting or not. -# if self.count < 6 { -# Some(self.count) -# } else { -# None -# } -# } -# } -let sum: u32 = Counter::new().zip(Counter::new().skip(1)) - .map(|(a, b)| a * b) - .filter(|x| x % 3 == 0) - .sum(); -assert_eq!(18, sum); -``` - -注意`zip`只生成四对值;理论上的第五对值并不会产生,因为`zip`在任一输入返回`None`时也会返回`None`(这个迭代器最多就生成 5)。 - -因为实现了`Iterator`的`next`方法,所有这些方法调用都是可能的。请查看标准库文档来寻找迭代器可能会用得上的方法。 \ No newline at end of file +在 Rust 中,迭代器是 **惰性的**(*lazy*),这意味着直到调用方法消费迭代器之前它都不会有效果。例如,列表 13-13 中的代码创建 \ No newline at end of file