@ -55,7 +55,7 @@ fn main() {
< span class = "caption" > 示例 13-2: `main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入</ span >
处 于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7; 一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数:
出 于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7; 一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数:
现在有了执行上下文,让我们编写算法。示例 13-3 中的 `generate_workout` 函数包含本例中我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中进行:
@ -96,7 +96,7 @@ fn generate_workout(intensity: u32, random_number: u32) {
< span class = "caption" > 示例 13-3: 程序的业务逻辑, 它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划</ span >
示例 13-3 中的代码有多处慢计算函数的调用 。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全 没有调用它,第二个 `else` 中的代码调用了它一次。
示例 13-3 中的代码有多处调用了慢计算函数 `simulated_expensive_calculation` 。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次, `else` 中的 `if` 没有调用它,而 第二个 `else` 中的代码调用了它一次。
<!-- NEXT PARAGRAPH WRAPPED WEIRD INTENTIONALLY SEE #199 -->
@ -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 中没有类型注解。
### 闭包类型推断和注解
@ -296,9 +296,9 @@ error[E0308]: mismatched types
幸运的是,还有另一个可用的方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation* 。
为了让结构体存放闭包,我们需要能够 指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。
为了让结构体存放闭包,我们需要指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。
`Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn` 、`FnMut` 或 `FnOnce` 中的一个。在“闭包会捕获其环境”部分捕获环境部分 我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。
`Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn` 、`FnMut` 或 `FnOnce` 中的一个。在“闭包会捕获其环境”部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。
为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `u32` 的参数并返回一个 `u32` ,这样所指定的 trait bound 就是 `Fn(u32) -> u32` 。
@ -319,7 +319,7 @@ struct Cacher<T>
结构体 `Cacher` 有一个泛型 `T` 的字段 `calculation` 。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `u32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `u32` (由 `->` 之后的内容)。
> 注意:函数也都实现了这三个 `Fn` trait。如果不需要捕获环境中的值, 则在需要实现 `Fn` trait 是可以使用 函数而不是闭包。
> 注意:函数也都实现了这三个 `Fn` trait。如果不需要捕获环境中的值, 则可以使用实现了 `Fn` trait 的 函数而不是闭包。
`value` 是 `Option<i32>` 类型的。在执行闭包之前,`value` 将是 `None` 。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。
@ -442,7 +442,7 @@ fn generate_workout(intensity: u32, random_number: u32) {
### `Cacher` 实现的限制
值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些 小问题,这使得在不同上下文中复用变得很困难。
值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在两个 小问题,这使得在不同上下文中复用变得很困难。
第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败:
@ -470,7 +470,7 @@ thread 'call_with_different_values' panicked at 'assertion failed: `(left == rig
这里的问题是第一次使用 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` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 函数会在哈希 map 中寻找 `arg` ,如果找到的话就返回其对应的值 。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。
当前 `Cacher` 实现的第二个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。
@ -527,7 +527,7 @@ error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
编译器甚至会提示我们这只能用于闭包!
当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,当执行不会捕获环境的更通用的代码场景中我们不希望有 这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。
当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。这会使用内存并产生额外的开销,在更一般的场景中,当我们不需要闭包来捕获环境时,我们不希望产生 这些开销。因为函数从未允许捕获环境,定义和使用函数也就从不会有这些额外开销。
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 `Fn` trait:
@ -557,7 +557,7 @@ fn main() {
}
```
这个例子并不能编译:
这个例子并不能编译,会产生以下错误 :
```text
error[E0382]: use of moved value: `x`
@ -575,6 +575,6 @@ error[E0382]: use of moved value: `x`
`x` 被移动进了闭包,因为闭包使用 `move` 关键字定义。接着闭包获取了 `x` 的所有权,同时 `main` 就不再允许在 `println!` 语句中使用 `x` 了。去掉 `println!` 即可修复问题。
大部分需要指定一个 `Fn` trait bound 的时候,可以从 `Fn` 开始,而编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce` 。
大部分需要指定一个 `Fn` 系列 trait bound 的时候,可以从 `Fn` 开始,而编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce` 。
为了展示闭包作为函数参数时捕获其环境的作用,让我们移动到 下一个主题:迭代器。
为了展示闭包作为函数参数时捕获其环境的作用,让我们继续 下一个主题:迭代器。