diff --git a/src/ch12-03-improving-error-handling-and-modularity.md b/src/ch12-03-improving-error-handling-and-modularity.md index eb455ce..bffb8d9 100644 --- a/src/ch12-03-improving-error-handling-and-modularity.md +++ b/src/ch12-03-improving-error-handling-and-modularity.md @@ -2,13 +2,13 @@ > [ch12-03-improving-error-handling-and-modularity.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-03-improving-error-handling-and-modularity.md) >
-> commit 5908c59a5a4cc58fd863605b80b295a335c2cbdf +> commit c1fb695e6c9091c9a5145320498ef80a649af33c 为了改善我们的程序这里有四个问题需要修复,而且他们都与程序的组织方式和如何处理潜在错误有关。 -第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立的任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。 +第一,`main` 现在进行了两个任务:它解析了参数并打开了文件。对于一个这样的小函数,这并不是一个大问题。然而如果 `main` 中的功能持续增加,`main` 函数处理的独立任务也会增加。当函数承担了更多责任,它就更难以推导,更难以测试,并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。 -这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构这样就能使他们的目的更明确了。 +这同时也关系到第二个问题:`search` 和 `filename` 是程序中的配置变量,而像 `f` 和 `contents` 则用来执行程序逻辑。随着 `main` 函数的增长,就需要引入更多的变量到作用域中,而当作用域中有更多的变量时,将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构,这样就能使他们的目的更明确了。 第三个问题是如果打开文件失败我们使用 `expect` 来打印出错误信息,不过这个错误信息只是说 `file not found`。除了缺少文件之外还有很多打开文件可能失败的方式:例如,文件可能存在,不过可能没有打开它的权限。如果我们现在就出于这种情况,打印出的 `file not found` 错误信息就给了用户错误的建议! @@ -22,18 +22,18 @@ 1. 将程序拆分成 *main.rs* 和 *lib.rs* 并将程序的逻辑放入 *lib.rs* 中。 2. 当命令行解析逻辑比较小时,可以保留在 *main.rs* 中。 -3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs*中。 +3. 当命令行解析开始变得复杂时,也同样将其从 *main.rs* 提取到 *lib.rs* 中。 4. 经过这些过程之后保留在 `main` 函数中的责任应该被限制为: * 使用参数值调用命令行解析逻辑 * 设置任何其他的配置 * 调用 *lib.rs* 中的 `run` 函数 * 如果 `run` 返回错误,则处理这个错误 -这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。 +这个模式的一切就是为了关注分离:*main.rs* 处理程序运行,而 *lib.rs* 处理所有的真正的任务逻辑。因为不能直接测试 `main` 函数,这个结构通过将所有的程序逻辑移动到 *lib.rs* 的函数中使得我们可以测试他们。仅仅保留在 *main.rs* 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序。 ### 提取参数解析器 -首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 做准备。示例 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中: +首先,我们将解析参数的功能提取到一个 `main` 将会调用的函数中,为将命令行解析逻辑移动到 *src/lib.rs* 中做准备。示例 12-5 中展示了新 `main` 函数的开头,它调用了新函数 `parse_config`。目前它仍将定义在 *src/main.rs* 中: 文件名: src/main.rs @@ -43,7 +43,7 @@ fn main() { let (query, filename) = parse_config(&args); - // ...snip... + // --snip-- } fn parse_config(args: &[String]) -> (&str, &str) { @@ -56,17 +56,17 @@ fn parse_config(args: &[String]) -> (&str, &str) { 示例 12-5:从 `main` 中提取出 `parse_config` 函数 -我们仍然将命令行参数收集进一个 vector,不过不同于在`main`函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `filename`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query` 和 `filename`,不过 `main` 不再负责处理命令行参数与变量如何对应。 +我们仍然将命令行参数收集进一个 vector,不过不同于在 `main` 函数中将索引 1 的参数值赋值给变量 `query` 和将索引 2 的值赋值给变量 `filename`,我们将整个 vector 传递给 `parse_config` 函数。接着 `parse_config` 函数将包含决定哪个参数该放入哪个变量的逻辑,并将这些值返回到 `main`。仍然在 `main` 中创建变量 `query` 和 `filename`,不过 `main` 不再负责处理命令行参数与变量如何对应。 -这对我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。 +这对重构我们这小程序可能有点大材小用,不过我们将采用小的、增量的步骤进行重构。在做出这些改变之后,再次运行程序并验证参数解析是否仍然正常。经常验证你的进展是一个好习惯,这样在遇到问题时能帮助你定位问题的成因。 ### 组合配置值 -我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又就将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。 +我们可以采取另一个小的步骤来进一步改善这个函数。现在函数返回一个元组,不过立刻又将元组拆成了独立的部分。这是一个我们可能没有进行正确抽象的信号。 另一个表明还有改进空间的迹象是 `parse_config` 名称的 `config` 部分,它暗示了我们返回的两个值是相关的并都是一个配置值的一部分。目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。这会让未来的维护者更容易理解不同的值如何相互关联以及他们的目的。 -> 注意:一些同学将这种拒绝使用相对而言更为合适的复合类型而使用基本类型的模式称为 **基本类型偏执**(*primitive obsession*)。 +> 注意:一些同学将这种在复杂类型更为合适的场景下使用基本类型的反模式称为 **基本类型偏执**(*primitive obsession*)。 示例 12-6 展示了新定义的结构体 `Config`,它有字段 `query` 和 `filename`。我们也改变了 `parse_config` 函数来返回一个 `Config` 结构体的实例,并更新 `main` 来使用结构体字段而不是单独的变量: @@ -86,7 +86,7 @@ fn main() { let mut f = File::open(config.filename).expect("file not found"); - // ...snip... + // --snip-- } struct Config { @@ -114,7 +114,7 @@ fn parse_config(args: &[String]) -> Config { 我们更新 `main` 将 `parse_config` 返回的 `Config` 实例放入变量 `config` 中,并将之前分别使用 `search` 和 `filename` 变量的代码更新为现在的使用 `Config` 结构体的字段的代码。 -现在代码更明确的表现了我们的意图,`query` 和 `filename` 是相关联的并且他们的目的是配置程序如何工作的。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。 +现在代码更明确的表现了我们的意图,`query` 和 `filename` 是相关联的并且他们的目的是配置程序如何工作。任何使用这些值的代码就知道在 `config` 实例中对应目的的字段名中寻找他们。 ### 创建一个 `Config` 构造函数 @@ -122,7 +122,6 @@ fn parse_config(args: &[String]) -> Config { 所以现在 `parse_config` 函数的目的是创建一个 `Config` 实例,我们可以将 `parse_config` 从一个普通函数变为一个叫做 `new` 的与结构体关联的函数。做出这个改变使得代码更符合习惯:可以像标准库中的 `String` 调用 `String::new` 来创建一个该类型的实例那样,将 `parse_config` 变为一个与 `Config` 关联的 `new` 函数。示例 12-7 展示了需要做出的修改: - 文件名: src/main.rs ```rust,should_panic @@ -133,7 +132,7 @@ fn main() { let config = Config::new(&args); - // ...snip... + // --snip-- } # struct Config { @@ -141,7 +140,7 @@ fn main() { # filename: String, # } # -// ...snip... +// --snip-- impl Config { fn new(args: &[String]) -> Config { @@ -159,50 +158,52 @@ impl Config { ### 修复错误处理 -现在我们开始修复错误处理。回忆一下之前提到过如果 `args` vector 包含少于 3 个项并尝试访问 vector 中索引 1 或 索引 2 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样: +现在我们开始修复错误处理。回忆一下之前提到过如果 `args` vector 包含少于 3 个项并尝试访问 vector 中索引 `1` 或索引 `2` 的值会造成程序 panic。尝试不带任何参数运行程序;这将看起来像这样: ```text $ cargo run + Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 -but the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307 +but the index is 1', src/main.rs:29:21 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` `index out of bounds: the len is 1 but the index is 1` 是一个针对程序员的错误信息,然而这并不能真正帮助终端用户理解发生了什么和他们应该做什么。现在就让我们修复它吧。 -### 改善错误信息 +#### 改善错误信息 在示例 12-8 中,在 `new` 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,我们使用一个更好的错误信息 panic 而不是 `index out of bounds` 信息: 文件名: src/main.rs ```rust,ignore -// ...snip... +// --snip-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("not enough arguments"); } - // ...snip... + // --snip-- ``` 示例 12-8:增加一个参数数量检查 -这类似于示例 9-8 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 +这类似于示例 9-9 中的 `Guess::new` 函数,那里如果 `value` 参数超出了有效值的范围就调用 `panic!`。不同于检查值的范围,这里检查 `args` 的长度至少是 3,而函数的剩余部分则可以在假设这个条件成立的基础上运行。如果 `args` 少于 3 个项,则这个条件将为真,并调用 `panic!` 立即终止程序。 有了 `new` 中这几行额外的代码,再次不带任何参数运行程序并看看现在错误看起来像什么: -```bash +```text $ cargo run + Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/minigrep` -thread 'main' panicked at 'not enough arguments', src/main.rs:29 +thread 'main' panicked at 'not enough arguments', src/main.rs:30:12 note: Run with `RUST_BACKTRACE=1` for a backtrace. ``` -这个输出就好多了,现在有了一个合理的错误信息。然而,我们还有一堆额外的信息不希望提供给用户。所以在这里使用示例 9-8 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用那一章学习的另一个技术:返回一个可以表明成功或错误的 `Result`。 +这个输出就好多了,现在有了一个合理的错误信息。然而,还是有一堆额外的信息我们不希望提供给用户。所以在这里使用示例 9-9 中的技术可能不是最好的;正如第九章所讲到的一样,`panic!` 的调用更趋向于程序上的问题而不是使用上的问题。相反我们可以使用第九章学习的另一个技术:返回一个可以表明成功或错误的 `Result`。 #### 从 `new` 中返回 `Result` 而不是调用 `panic!` @@ -229,13 +230,13 @@ impl Config { 示例 12-9:从 `Config::new` 中返回 `Result` -现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是一个字符串字面值,也是目前的错误信息。 +现在 `new` 函数返回一个 `Result`,在成功时带有一个 `Config` 实例而在出现错误时带有一个 `&'static str`。回忆一下第十章 “静态生命周期” 中讲到 `&'static str` 是字符串字面值的类型,也是目前的错误信息。 `new` 函数体中有两处修改:当没有足够参数时不再调用 `panic!`,而是返回 `Err` 值。同时我们将 `Config` 返回值包装进 `Ok` 成员中。这些修改使得函数符合其新的类型签名。 通过让 `Config::new` 返回一个 `Err` 值,这就允许 `main` 函数处理 `new` 函数返回的 `Result` 值并在出现错误的情况更明确的结束进程。 -### `Config::new` 调用并处理错误 +#### `Config::new` 调用并处理错误 为了处理错误情况并打印一个对用户友好的信息,我们需要像示例 12-10 那样更新 `main` 函数来处理现在 `Config::new` 返回的 `Result`。另外还需要负责手动实现 `panic!` 的使用非零错误码退出命令行工具的工作。非零的退出状态是一个告诉调用程序的进程我们的程序以错误状态退出的惯例信号。 @@ -252,7 +253,7 @@ fn main() { process::exit(1); }); - // ...snip... + // --snip-- ``` 示例 12-10:如果新建 `Config` 失败则使用错误码退出 @@ -281,7 +282,7 @@ Problem parsing arguments: not enough arguments ```rust,ignore fn main() { - // ...snip... + // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.filename); @@ -299,7 +300,7 @@ fn run(config: Config) { println!("With text:\n{}", contents); } -// ...snip... +// --snip-- ``` 示例 12-11:提取 `run` 函数来包含剩余的程序逻辑 @@ -315,7 +316,7 @@ fn run(config: Config) { ```rust,ignore use std::error::Error; -// ...snip... +// --snip-- fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; @@ -330,7 +331,8 @@ fn run(config: Config) -> Result<(), Box> { ``` 示例 12-12:修改 `run` 函数返回 `Result` - `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 + +这里我们做出了三个明显的修改。首先,将 `run` 函数的返回类型变为 `Result<(), Box>`。之前这个函数返回 unit 类型 `()`,现在它仍然保持作为 `Ok` 时的返回值。 对于错误类型,使用了 **trait 对象** `Box`(在开头使用了 `use` 语句将 `std::error::Error` 引入作用域)。第十七章会涉及 trait 对象。目前只需知道 `Box` 意味着函数会返回实现了 `Error` trait 的类型,不过无需指定具体将会返回的值的类型。这提供了在不同的错误场景可能有不同类型的错误返回值的灵活性。 @@ -341,12 +343,12 @@ fn run(config: Config) -> Result<(), Box> { 上述代码能够编译,不过会有一个警告: ```text -warning: unused result which must be used, #[warn(unused_must_use)] on by -default - --> src/main.rs:39:5 +warning: unused `std::result::Result` which must be used + --> src/main.rs:18:5 | -39 | run(config); +18 | run(config); | ^^^^^^^^^^^^ += note: #[warn(unused_must_use)] on by default ``` Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在一个错误。虽然我们没有检查这里是否有一个错误,而编译器提醒我们这里应该有一些错误处理代码!现在就让我们修正他们。 @@ -359,7 +361,7 @@ Rust 提示我们的代码忽略了 `Result` 值,它可能表明这里存在 ```rust,ignore fn main() { - // ...snip... + // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.filename); @@ -372,22 +374,22 @@ fn main() { } ``` -我们使用 `if let` 来检查 `run` 是否返回一个 `Err` 值,不同于 `unwrap_or_else`,并在出错时调用 `process::exit(1)`。`run` 并不返回像 `Config::new` 返回的 `Config` 实例那样需要 `unwrap` 的值。因为 `run` 在成功时返回 `()`,而我们只关心发现一个错误,所以并不需要 `unwrap_or_else` 来返回未封装的值,因为它只会是 `()`。 +我们使用 `if let` 来检查 `run` 是否返回一个 `Err` 值,不同于 `unwrap_or_else`,并在出错时调用 `process::exit(1)`。`run` 并不返回像 `Config::new` 返回的 `Config` 实例那样需要 `unwrap` 的值。因为 `run` 在成功时返回 `()`,而我们只关心检测错误,所以并不需要 `unwrap_or_else` 来返回未封装的值,因为它只会是 `()`。 不过两个例子中 `if let` 和 `unwrap_or_else` 的函数体都一样:打印出错误并退出。 ### 将代码拆分到库 crate -现在项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个拥有更少功能的 `main` 函数。 +现在我们的 `minigrep` 项目看起来好多了!现在我们将要拆分 *src/main.rs* 并将一些代码放入 *src/lib.rs*,这样就能测试他们并拥有一个含有更少功能的 `main` 函数。 -让我们将如下代码片段从 *src/main.rs* 移动到新文件 *src/lib.rs* 中: +让我们将所有不是 `main` 函数的代码从 *src/main.rs* 移动到新文件 *src/lib.rs* 中: - `run` 函数定义 - 相关的 `use` 语句 - `Config` 的定义 - `Config::new` 函数定义 -现在 *src/lib.rs* 的内容应该看起来像示例 12-13(为了简洁省略了函数体): +现在 *src/lib.rs* 的内容应该看起来像示例 12-13(为了简洁省略了函数体)。注意直到下一个示例修改完 *src/main.rs* 之后,代码还不能编译: 文件名: src/lib.rs @@ -403,20 +405,20 @@ pub struct Config { impl Config { pub fn new(args: &[String]) -> Result { - // ...snip... + // --snip-- } } pub fn run(config: Config) -> Result<(), Box> { - // ...snip... + // --snip-- } ``` 示例 12-13:将 `Config` 和 `run` 移动到 *src/lib.rs* -这里使用了公有的 `pub`:在 `Config`、其字段和其 `new`方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 +这里使用了公有的 `pub`:在 `Config`、其字段和其 `new` 方法,以及 `run` 函数上。现在我们有了一个拥有可以测试的公有 API 的库 crate 了。 -现在需要在 *src/main.rs* 中使用 `extern crate minigrep` 将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域。接着我们将增加一个 `use minigrep::Config` 行将 `Config` 类型引入作用域,并使用库 crate 的名称作为 `run` 函数的前缀,如示例 12-14 所示: +现在需要在 *src/main.rs* 中将移动到 *src/lib.rs* 的代码引入二进制 crate 的作用域中,如示例 12-14 所示: Filename: src/main.rs @@ -429,17 +431,17 @@ use std::process; use minigrep::Config; fn main() { - // ...snip... + // --snip-- if let Err(e) = minigrep::run(config) { - // ...snip... + // --snip-- } } ``` -示例 12-14:将 `minigrep` crate 引入 *src/main.rs* 的作用域 +示例 12-14:将 `minigrep` crate 引入 *src/main.rs* 的作用域中 为了将库 crate 引入二进制 crate,我们使用 `extern crate minigrep`。接着增加 `use minigrep::Config` 将 `Config` 类型引入作用域,并使用 crate 名作为 `run` 函数的前缀。通过这些重构,所有功能应该能够联系在一起并运行了。运行 `cargo run` 来确保一切都正确的衔接在一起。 -哇哦!这可有很多的工作,不过我们为将来成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。 +哇哦!这可有很多的工作,不过我们为将来的成功打下了基础。现在处理错误将更容易,同时代码也更加模块化。从现在开始几乎所有的工作都将在 *src/lib.rs* 中进行。 让我们利用这些新创建的模块的优势来进行一些在旧代码中难以展开的工作,他们在新代码中却很简单:编写测试! diff --git a/src/ch12-04-testing-the-librarys-functionality.md b/src/ch12-04-testing-the-librarys-functionality.md index 810c6d2..903aaf5 100644 --- a/src/ch12-04-testing-the-librarys-functionality.md +++ b/src/ch12-04-testing-the-librarys-functionality.md @@ -1,17 +1,17 @@ -## 测试库的功能 +## 采用测试驱动开发完善库的功能 > [ch12-04-testing-the-librarys-functionality.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-04-testing-the-librarys-functionality.md) >
-> commit 5908c59a5a4cc58fd863605b80b295a335c2cbdf +> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 现在我们将逻辑提取到了 *src/lib.rs* 并将所有的参数解析和错误处理留在了 *src/main.rs* 中,为代码的核心功能编写测试将更加容易。我们可以直接使用多种参数调用函数并检查返回值而无需从命令行运行二进制文件了。如果你愿意的话,请自行为 `Config::new` 和 `run` 函数的功能编写一些测试。 -在这一部分,我们将遵循测试驱动开发(Test Driven Development, TTD)的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤: +在这一部分,我们将遵循测试驱动开发(Test Driven Development, TDD)的模式来逐步增加 `minigrep` 的搜索逻辑。这是一个软件开发技术,它遵循如下步骤: 1. 编写一个会失败的测试,并运行它以确保其因为你期望的原因失败。 2. 编写或修改刚好足够的代码来使得新的测试通过。 3. 重构刚刚增加或修改的代码,并确保测试仍然能通过。 -4. 重复上述步骤! +4. 从步骤 1 开始重复! 这只是众多编写软件的方法之一,不过 TDD 有助于驱动代码的设计。在编写能使测试通过的代码之前编写测试有助于在开发过程中保持高测试覆盖率。 @@ -19,7 +19,7 @@ ### 编写失败测试 -首先,去掉 *src/lib.rs* 和 *src/main.rs* 中的`println!`语句,因为不再真正需要他们了。接着我们会像第十一章那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试: +去掉 *src/lib.rs* 和 *src/main.rs* 中用于检查程序行为的 `println!` 语句,因为不再真正需要他们了。接着我们会像第十一章那样增加一个 `test` 模块和一个测试函数。测试函数指定了 `search` 函数期望拥有的行为:它会获取一个需要查询的字符串和用来查询的文本,并只会返回包含请求的文本行。示例 12-15 展示了这个测试,它还不能编译: 文件名: src/lib.rs @@ -52,11 +52,11 @@ Pick three."; 这里选择使用 "duct" 作为这个测试中需要搜索的字符串。用来搜索的文本有三行,其中只有一行包含 "duct"。我们断言 `search` 函数的返回值只包含期望的那一行。 -我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。一旦有了它,这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 +我们还不能运行这个测试并看到它失败,因为它甚至都还不能编译!我们将增加足够的代码来使其能够编译:一个总是会返回空 vector 的 `search` 函数定义,如示例 12-16 所示。然后这个测试应该能够编译并因为空 vector 并不匹配一个包含一行 `"safe, fast, productive."` 的 vector 而失败。 文件名: src/lib.rs -``` +```rust pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } @@ -72,10 +72,11 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { ```text error[E0106]: missing lifetime specifier - --> src/lib.rs:5:47 + --> src/lib.rs:5:51 | -5 | fn search(query: &str, contents: &str) -> Vec<&str> { - | ^ expected lifetime parameter +5 | pub fn search(query: &str, contents: &str) -> Vec<&str> { + | ^ expected lifetime +parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` @@ -83,13 +84,14 @@ error[E0106]: missing lifetime specifier Rust 不可能知道我们需要的是哪一个参数,所以需要告诉它。因为参数 `contents` 包含了所有的文本而且我们希望返回匹配的那部分文本,所以我们知道 `contents` 是应该要使用生命周期语法来与返回值相关联的参数。 -其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移会越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。 +其他语言中并不需要你在函数签名中将参数与返回值相关联,所以这么做可能仍然感觉有些陌生,随着时间的推移这将会变得越来越容易。你可能想要将这个例子与第十章中生命周期语法部分做对比。 -现在试尝试运行测试: +现在运行测试: ```text $ cargo test -...warnings... + Compiling minigrep v0.1.0 (file:///projects/minigrep) +--warnings-- Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs Running target/debug/deps/minigrep-abcabcabc @@ -99,17 +101,19 @@ test test::one_result ... FAILED failures: ---- test::one_result stdout ---- - thread 'test::one_result' panicked at 'assertion failed: `(left == right)` -(left: `["safe, fast, productive."]`, right: `[]`)', src/lib.rs:16 + thread 'test::one_result' panicked at 'assertion failed: `(left == +right)` +left: `["safe, fast, productive."]`, +right: `[]`)', src/lib.rs:48:8 note: Run with `RUST_BACKTRACE=1` for a backtrace. failures: test::one_result -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out -error: test failed +error: test failed, to rerun pass '--lib' ``` 好的,测试失败了,这正是我们所期望的。修改代码来让测试通过吧! @@ -118,19 +122,19 @@ error: test failed 目前测试之所以会失败是因为我们总是返回一个空的 vector。为了修复并实现 `search`,我们的程序需要遵循如下步骤: -* 遍历每一行文本。 +* 遍历内容的每一行文本。 * 查看这一行是否包含要搜索的字符串。 -* 如果有,将这一行加入返回示例中。 +* 如果有,将这一行加入列表返回值中。 * 如果没有,什么也不做。 -* 返回匹配到的示例 +* 返回匹配到的结果列表 让我们一步一步的来,从遍历每行开始。 #### 使用 `lines` 方法遍历每一行 -Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作: +Rust 有一个有助于一行一行遍历字符串的方法,出于方便它被命名为 `lines`,它如示例 12-17 这样工作。注意这还不能编译: -Filename: src/lib.rs +文件名: src/lib.rs ```rust,ignore pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { @@ -142,11 +146,11 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { 示例 12-17:遍历 `contents` 的每一行 -`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-6 中见过使用迭代器的方法,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 +`lines` 方法返回一个迭代器。第十三章会深入了解迭代器,不过我们已经在示例 3-4 中见过使用迭代器的方法了,在那里使用了一个 `for` 循环和迭代器在一个集合的每一项上运行了一些代码。 #### 用查询字符串搜索每一行 -接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如示例 12-18 所示在 `search` 函数中加入 `contains` 方法调用: +接下来将会增加检查当前行是否包含查询字符串的功能。幸运的是,字符串类型为此也有一个叫做 `contains` 的实用方法!如示例 12-18 所示在 `search` 函数中加入 `contains` 方法调用。注意这仍然不能编译: 文件名: src/lib.rs @@ -164,7 +168,7 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { #### 存储匹配的行 -最后我们需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector,如示例 12-19 所示: +我们还需要一个方法来存储包含查询字符串的行。为此可以在 `for` 循环之前创建一个可变的 vector 并调用 `push` 方法在 vector 中存放一个 `line`。在 `for` 循环之后,返回这个 vector,如示例 12-19 所示: 文件名: src/lib.rs @@ -188,15 +192,16 @@ pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { ```text $ cargo test +--snip-- running 1 test test test::one_result ... ok -test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` -测试通过了,很好,它可以工作了! +测试通过了,它可以工作了! -现在测试通过了,我们可以考虑一下重构 `search` 的实现并时刻保持测试通过来保持其功能不变的机会了。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 +到此为止,我们可以考虑一下重构 `search` 的实现并时刻保持测试通过来保持其功能不变的机会了。`search` 函数中的代码并不坏,不过并没有利用迭代器的一些实用功能。第十三章将回到这个例子并深入探索迭代器并看看如何改进代码。 #### 在 `run` 函数中使用 `search` 函数 @@ -221,7 +226,7 @@ pub fn run(config: Config) -> Result<(), Box> { 这里仍然使用了 `for` 循环获取了 `search` 返回的每一行并打印出来。 -现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 "frog": +现在整个程序应该可以工作了!让我们试一试,首先使用一个只会在艾米莉·狄金森的诗中返回一行的单词 “frog”: ```text $ cargo run frog poem.txt @@ -231,14 +236,15 @@ $ cargo run frog poem.txt How public, like a frog ``` -好的!接下来,像 "the" 这样会匹配多行的单词会怎么样呢: +好的!现在试试一个会匹配多行的单词,比如 “body”: ```text -$ cargo run the poem.txt +$ cargo run body poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs - Running `target/debug/minigrep the poem.txt` -Then there’s a pair of us — don’t tell! -To tell your name the livelong day + Running `target/debug/minigrep body poem.txt` +I’m nobody! Who are you? +Are you nobody, too? +How dreary to be somebody! ``` 最后,让我们确保搜索一个在诗中哪里都没有的单词时不会得到任何行,比如 "monomorphization": @@ -251,4 +257,4 @@ $ cargo run monomorphization poem.txt 非常好!我们创建了一个属于自己的迷你版经典工具,并学习了很多如何组织程序的知识。我们还学习了一些文件输入输出、生命周期、测试和命令行解析的内容。 -为了使这个项目章节更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。现在如果你希望的话请随意移动到第十三章。 +为了使这个项目更丰满,我们将简要的展示如何处理环境变量和打印到标准错误,这两者在编写命令行程序时都很有用。 diff --git a/src/ch12-05-working-with-environment-variables.md b/src/ch12-05-working-with-environment-variables.md index 422d9c9..a61ef43 100644 --- a/src/ch12-05-working-with-environment-variables.md +++ b/src/ch12-05-working-with-environment-variables.md @@ -2,15 +2,13 @@ > [ch12-05-working-with-environment-variables.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-05-working-with-environment-variables.md) >
-> commit adababc48956f4d39c97c8b6fc14a104d90e20dc +> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 -我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 +我们将增加一个额外的功能来改进 `minigrep`:一个通过环境变量启用的大小写不敏感搜索的选项。可以将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变量一次之后在整个终端会话中所有的搜索都将是大小写不敏感的。 ### 编写一个大小写不敏感 `search` 函数的失败测试 -首先,增加一个新函数,当设置了环境变量时会调用它。 - -这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如示例 12-20 所示: +我们希望增加一个新函数 `search_case_insensitive`,并将会在设置了环境变量时调用它。这里将继续遵循 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从 `one_result` 改名为 `case_sensitive` 来更清楚的表明这两个测试的区别,如示例 12-20 所示: 文件名: src/lib.rs @@ -93,7 +91,7 @@ running 2 tests test test::case_insensitive ... ok test test::case_sensitive ... ok -test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out ``` 好的!现在,让我们在 `run` 函数中实际调用新 `search_case_insensitive` 函数。首先,我们将在 `Config` 结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索: @@ -108,7 +106,7 @@ pub struct Config { } ``` -这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示: +这里增加了 `case_sensitive` 字符来存放一个布尔值。接着我们需要 `run` 函数检查 `case_sensitive` 字段的值并使用它来决定是否调用 `search` 函数或 `search_case_insensitive` 函数,如示例 12-22 所示。注意这还不能编译: 文件名: src/lib.rs @@ -131,7 +129,7 @@ pub struct Config { # case_sensitive: bool, # } # -pub fn run(config: Config) -> Result<(), Box>{ +pub fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; let mut contents = String::new(); @@ -165,7 +163,7 @@ use std::env; # case_sensitive: bool, # } -// ...snip... +// --snip-- impl Config { pub fn new(args: &[String]) -> Result { @@ -195,6 +193,7 @@ impl Config { ```text $ cargo run to poem.txt + Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/minigrep to poem.txt` Are you nobody, too? @@ -213,6 +212,13 @@ To tell your name the livelong day To an admiring bog! ``` +如果你使用 PowerShell,则需要用两句命令而不是一句来设置环境变量并运行程序: + +```text +$ $env.CASE_INSENSITIVE=1 +$ cargo run to poem.txt +``` + 好极了,我们也得到了包含 “To” 的行!现在 `minigrep` 程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了! 一些程序允许对相同配置同时使用参数 **和** 环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试通过一个命令行参数或一个环境变量来控制大小写不敏感搜索。并在运行程序时遇到矛盾值时决定命令行参数和环境变量的优先级。 diff --git a/src/ch12-06-writing-to-stderr-instead-of-stdout.md b/src/ch12-06-writing-to-stderr-instead-of-stdout.md index ed39973..3decb28 100644 --- a/src/ch12-06-writing-to-stderr-instead-of-stdout.md +++ b/src/ch12-06-writing-to-stderr-instead-of-stdout.md @@ -1,24 +1,26 @@ -## 输出到`stderr`而不是`stdout` +## 将错误信息输出到标准错误而不是标准输出 > [ch12-06-writing-to-stderr-instead-of-stdout.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch12-06-writing-to-stderr-instead-of-stdout.md) >
-> commit 7db14aa689553706198ffcb11a8c60b478e752fe +> commit 1fe78a83f37ecc69b840fdc8dcfc727f88a3a3d4 -目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*)对应大部分信息(有时在代码中使用缩写 `stdout`),**标准错误**(*standard error*)则用于错误信息(`stderr`)。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 +目前为止,我们将所有的输出都 `println!` 到了终端。大部分终端都提供了两种输出:**标准输出**(*standard output*,`stdout`)对应通用信息,**标准错误**(*standard error*,`stderr`)则用于错误信息。这种区别允许用户选择将程序正常输出定向到一个文件中并仍将错误信息打印到屏幕上。 但是 `println!` 函数只能够打印到标准输出,所以我们必需使用其他方法来打印到标准错误。 ### 检查错误应该写入何处 -首先,让我们观察一下目前 `minigrep` 打印的所有内容都被写入了标准输出,包括应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;我们将看到相反它将错误信息输出保存到了文件中。 +首先,让我们观察一下目前 `minigrep` 打印的所有内容都被写入了标准输出,包括应该被写入标准错误的错误信息。可以通过将标准输出流重定向到一个文件同时有意产生一个错误来做到这一点。我们没有重定向标准错误流,所以任何发送到标准错误的内容将会继续显示在屏幕上。 -展示这种行为的方式是通过 `>` 和文件名 *output.txt* 来与运行程序,这个文件是期望重定向标准输出流的位置。并不传递任何参数这样应该会产生一个错误: +命令行程序被期望将错误信息发送到标准错误流,这样即便选择将标准输出流重定向到文件中时仍然能看到错误信息。目前我们的程序并不符合期望;相反我们将看到它将错误信息输出保存到了文件中。 + +展示这种行为的方式是通过 `>` 和文件名 *output.txt* 来与运行程序,这个文件是期望重定向标准输出流的位置。并不传递任何参数应该会产生一个错误: ```text $ cargo run > output.txt ``` -`>` 语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。让我们看看 *output.txt* 包含什么: +`>` 语法告诉 shell 将标准输出的内容写入到 *output.txt* 文件中而不是屏幕上。我们并没有看到期望的错误信息打印到屏幕上,所以这意味着它一定被写入了文件中。如下是 *output.txt* 所包含的: ```text Problem parsing arguments: not enough arguments @@ -28,7 +30,7 @@ Problem parsing arguments: not enough arguments ### 将错误打印到标准错误 -让我们如示例 12-24 所示的代码改变错误信息是如何被打印的。得益于本章早些时候的重构,所有打印错误信息的代码都位于 `main` 一个函数中。标准库提供了 `eprintln!` 宏来打印到标准错误流,所以将两个调用 `println!` 打印错误信息的维持替换为 `eprintln!`: +让我们如示例 12-24 所示的代码改变错误信息是如何被打印的。得益于本章早些时候的重构,所有打印错误信息的代码都位于 `main` 一个函数中。标准库提供了 `eprintln!` 宏来打印到标准错误流,所以将两个调用 `println!` 打印错误信息的位置替换为 `eprintln!`: 文件名: src/main.rs @@ -58,9 +60,9 @@ $ cargo run > output.txt Problem parsing arguments: not enough arguments ``` -现在我们看到了屏幕上的错误信息,同时 `output.txt` 里什么也没有,这也就是命令行程序所期望的行为。 +现在我们看到了屏幕上的错误信息,同时 `output.txt` 里什么也没有,这正是命令行程序所期望的行为。 -如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件: +如果使用不会造成错误的参数再次运行程序,不过仍然将标准输出重定向到一个文件,像这样: ```text $ cargo run to poem.txt > output.txt @@ -75,10 +77,10 @@ Are you nobody, too? How dreary to be somebody! ``` -这一部分展示了现在我们适当的使用成功时产生的标准输出和错误时产生的标准错误。 +这一部分展示了现在我们适当的使用了成功时产生的标准输出和错误时产生的标准错误。 ## 总结 在这一章中,我们回顾了目前为止的一些主要章节并涉及了如何在 Rust 环境中进行常规的 I/O 操作。通过使用命令行参数、文件、环境变量和打印错误的 `eprintln!` 宏,现在你已经准备好编写命令行程序了。通过结合前几章的知识,你的代码将会是组织良好的,并能有效的将数据存储到合适的数据结构中、更好的处理错误,并且还是经过良好测试的。 -接下来,让我们探索如何利用一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 \ No newline at end of file +接下来,让我们探索一些 Rust 中受函数式编程语言影响的功能:闭包和迭代器。 \ No newline at end of file diff --git a/src/ch13-00-functional-features.md b/src/ch13-00-functional-features.md index e7b91b3..6de9f6e 100644 --- a/src/ch13-00-functional-features.md +++ b/src/ch13-00-functional-features.md @@ -1,16 +1,16 @@ -# Rust 中的函数式语言功能 —— 迭代器和闭包 +# Rust 中的函数式语言功能:迭代器与闭包 > [ch13-00-functional-features.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-00-functional-features.md) >
-> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9 +> commit 2bcb126815a381acc3d46b0d6fc382cb4c98fbc5 -Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。我们不会在这里讨论函数式编程是或不是什么的问题,而是展示Rust的一些在功能上与其他语言类似的特性。 +Rust 的设计灵感来源于很多现存的语言和技术。其中一个显著的影响就是 **函数式编程**(*functional programming*)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等等。我们不会在这里讨论函数式编程是或不是什么的问题,而是展示 Rust 的一些在功能上与其他被认为是函数式语言类似的特性。 更具体的,我们将要涉及: -* **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构。 -* **迭代器**(*Iterators*),一种处理元素序列的方式。。 +* **闭包**(*Closures*),一个可以储存在变量里的类似函数的结构 +* **迭代器**(*Iterators*),一种处理元素序列的方式 * 如何使用这些功能来改进第十二章的 I/O 项目。 -* 这些功能的性能。**剧透高能:** 他们的速度超乎你的想象! +* 这两个功能的性能。(**剧透高能:** 他们的速度超乎你的想象!) 还有其它受函数式风格影响的 Rust 功能,比如模式匹配和枚举,这些已经在其他章节中讲到过了。掌握闭包和迭代器则是编写符合语言风格的高性能 Rust 代码的重要一环,所以我们将专门用一整章来讲解他们。 diff --git a/src/ch13-01-closures.md b/src/ch13-01-closures.md index 58e71d3..9be96af 100644 --- a/src/ch13-01-closures.md +++ b/src/ch13-01-closures.md @@ -2,15 +2,17 @@ > [ch13-01-closures.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch13-01-closures.md) >
-> commit 56352c28cf3fe0402fa5a7cba73890e314d720eb +> commit f23a91d6a2f37ba6d415d2c8ca4302bf1b3a4e9e -Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在以不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。 +Rust 的 **闭包**(*closures*)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的值。我们将展示闭包的这些功能如何复用代码和自定义行为。 ### 使用闭包创建行为的抽象 -让我们看看一个展示储存闭包并在之后执行如何有用的情形的例子。其间我们会讨论闭包的语法、类型推断和 trait。 +让我们看看一个展示储存闭包并在之后执行是如何有价值的用例。其间我们会讨论闭包的语法、类型推断和 trait。 -这个假想的情况如下:我们在一个通过 app 生成自定义健身计划的初创企业工作。其后端使用 Rust 编写,而生成健身计划的算法需要考虑很多不同的因素,比如用户的年龄、身体质量指数(Body Mass Index)、用户喜好、最近的健身活动和用户指定的强度系数。本例中实际的算法并不重要。我们只希望在需要时调用算法,并且只希望调用一次,这样就不会让用户等得太久。这里将通过调用 `simulated_expensive_calculation` 函数来模拟调用假象的算法,如示例 13-1 所示,它会打印出 `calculating slowly...`,等待两秒,并接着返回传递给它的数字: +考虑一下这个假想的情况:我们在一个通过 app 生成自定义健身计划的初创企业工作。其后端使用 Rust 编写,而生成健身计划的算法需要考虑很多不同的因素,比如用户的年龄、身体质量指数(Body Mass Index)、用户喜好、最近的健身活动和用户指定的强度系数。本例中实际的算法并不重要,重要的是这个计算只花费几秒钟。我们只希望在需要时调用算法,并且只希望调用一次,这样就不会让用户等得太久。 + +这里将通过调用 `simulated_expensive_calculation` 函数来模拟调用假象的算法,如示例 13-1 所示,它会打印出 `calculating slowly...`,等待两秒,并接着返回传递给它的数字: 文件名: src/main.rs @@ -18,25 +20,23 @@ Rust 的闭包是可以保存进变量或作为参数传递给其他函数的匿 use std::thread; use std::time::Duration; -fn simulated_expensive_calculation(intensity: i32) -> i32 { +fn simulated_expensive_calculation(intensity: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity } ``` -示例 13-1:一个用来代替假象计算的函数,它大约会执行两秒 +示例 13-1:一个用来代替假象计算的函数,它大约会执行两秒钟 接下来,`main` 函数中将会包含本例的健身 app 中的重要部分。这代表当用户请求健身计划时 app 会调用的代码。因为与 app 前端的交互与闭包的使用并不相关,所以我们将硬编码代表程序输入的值并打印输出。 -程序的输入有: +所需的输入有: -- 一个来自用户的 `intensity` 数字,请求健身计划时指定,它代表用户喜好低强度还是高强度健身 -- 一个会在健身计划中生成变化的随机数 +* **一个来自用户的 intensity 数字**,请求健身计划时指定,它代表用户喜好低强度还是高强度健身。 +* **一个随机数**,其会在健身计划中生成变化。 -程序的输出将会是建议的锻炼计划。 - -示例 13-2 展示了我们将要使用的 `main` 函数。处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: +程序的输出将会是建议的锻炼计划。示例 13-2 展示了我们将要使用的 `main` 函数: 文件名: src/main.rs @@ -45,14 +45,19 @@ fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; - generate_workout(simulated_user_specified_value, simulated_random_number); + generate_workout( + simulated_user_specified_value, + simulated_random_number + ); } -# fn generate_workout(intensity: i32, random_number: i32) {} +# fn generate_workout(intensity: u32, random_number: u32) {} ``` 示例 13-2:`main` 函数包含了用于 `generate_workout` 函数的模拟用户输入和模拟随机数输入 -这就是我们的执行上下文。示例 13-3 中的 `generate_workout` 函数包含我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中: +处于简单考虑这里硬编码了 `simulated_user_specified_value` 变量的值为 10 和 `simulated_random_number` 变量的值为 7;一个实际的程序会从 app 前端获取强度系数并使用 `rand` crate 来生成随机数,正如第二章的猜猜看游戏所做的那样。`main` 函数使用模拟的输入值调用 `generate_workout` 函数: + +现在有了执行上下文,让我们编写算法。示例 13-3 中的 `generate_workout` 函数包含本例中我们最关心的 app 业务逻辑。本例中余下的代码修改都将在这个函数中进行: 文件名: src/main.rs @@ -60,13 +65,13 @@ fn main() { # use std::thread; # use std::time::Duration; # -# fn simulated_expensive_calculation(num: i32) -> i32 { +# fn simulated_expensive_calculation(num: u32) -> u32 { # println!("calculating slowly..."); # thread::sleep(Duration::from_secs(2)); # num # } # -fn generate_workout(intensity: i32, random_number: i32) { +fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( "Today, do {} pushups!", @@ -91,13 +96,19 @@ fn generate_workout(intensity: i32, random_number: i32) { 示例 13-3:程序的业务逻辑,它根据输入并调用 `simulated_expensive_calculation` 函数来打印出健身计划 -示例 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,`else` 中的 `else` 调用了它一次。 +示例 13-3 中的代码有多处慢计算函数的调用。第一个 `if` 块调用了 `simulated_expensive_calculation` 两次,外部 `else` 中的 `if` 完全没有调用它,第二个 `else` 中的代码调用了它一次。 -`generate_workout` 函数的期望行为是首先检查用户需要低强度(由小于 25 的系数表示)锻炼还是高强度(25 或以上)锻炼。低强度锻炼计划会根据由 `simulated_expensive_calculation` 函数所模拟的复杂算法建议一定数量的俯卧撑和仰卧起坐,此函数需要强度系数作为输入。 + + +`generate_workout` 函数的期望行为是首先检查用户需要低强度(由小于 25 的系数表示)锻炼还是高强度(25 或以上)锻炼。 + +低强度锻炼计划会根据由 `simulated_expensive_calculation` 函数所模拟的复杂算法建议一定数量的俯卧撑和仰卧起坐。 如果用户需要高强度锻炼,这里有一些额外的逻辑:如果 app 生成的随机数刚好是 3,app 相反会建议用户稍做休息并补充水分。如果不是,则用户会从复杂算法中得到数分钟跑步的高强度锻炼计划。 -数据科学部门的同学告知我们将来会对调用算法的方式做出一些改变。为了在要做这些改动的时候简化更新步骤,我们将重构代码来让它只调用 `simulated_expensive_calculation` 一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过最终仍然需要调用函数一次。 +数据科学部门的同学告知我们将来会对调用算法的方式做出一些改变。为了在要做这些改动的时候简化更新步骤,我们将重构代码来让它只调用 `simulated_expensive_calculation` 一次。同时还希望去掉目前多余的连续两次函数调用,并不希望在计算过程中增加任何其他此函数的调用。也就是说,我们不希望在完全无需其结果的情况调用函数,不过仍然希望只调用函数一次。 + +#### 使用函数重构 有多种方法可以重构此程序。我们首先尝试的是将重复的慢计算函数调用提取到一个变量中,如示例 13-4 所示: @@ -107,13 +118,13 @@ fn generate_workout(intensity: i32, random_number: i32) { # use std::thread; # use std::time::Duration; # -# fn simulated_expensive_calculation(num: i32) -> i32 { +# fn simulated_expensive_calculation(num: u32) -> u32 { # println!("calculating slowly..."); # thread::sleep(Duration::from_secs(2)); # num # } # -fn generate_workout(intensity: i32, random_number: i32) { +fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); @@ -133,19 +144,19 @@ fn generate_workout(intensity: i32, random_number: i32) { println!( "Today, run for {} minutes!", expensive_result - ) + ); } } } ``` -示例 13-4:将 `simulated_expensive_calculation` 调用提取到一个位置,位于 `if` 块之前并将结果储存在变量 `expensive_result` 中 +示例 13-4:将 `simulated_expensive_calculation` 调用提取到一个位置,并将结果储存在变量 `expensive_result` 中 这个修改统一了 `simulated_expensive_calculation` 调用并解决了第一个 `if` 块中不必要的两次调用函数的问题。不幸的是,现在所有的情况下都需要调用函数并等待结果,包括那个完全不需要这一结果的内部 `if` 块。 -我们希望能够在程序的一个位置指定某些代码,并只在程序的某处实际需要结果的时候执行这些代码。这正是闭包的用武之地! +我们希望能够在程序的一个位置指定某些代码,并只在程序的某处实际需要结果的时候 **执行** 这些代码。这正是闭包的用武之地! -### 闭包储存了之后会执行的代码 +#### 重构使用闭包储存代码 不同于总是在 `if` 块之前调用 `simulated_expensive_calculation` 函数并储存其结果,我们可以定义一个闭包并将其储存在变量中,如示例 13-5 所示。实际上可以选择将整个 `simulated_expensive_calculation` 函数体移动到这里引入的闭包中: @@ -163,15 +174,15 @@ let expensive_closure = |num| { # expensive_closure(5); ``` -示例 13-5:使用慢计算的函数体定义一个闭包并储存到变量 `expensive_closure` 中 +示例 13-5:定义一个闭包并储存到变量 `expensive_closure` 中 -闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始。在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`。 +闭包定义是 `expensive_closure` 赋值的 `=` 之后的部分。闭包的定义以一对竖线(`|`)开始,在竖线中指定闭包的参数;之所以选择这个语法是因为它与 Smalltalk 和 Ruby 的闭包定义类似。这个闭包有一个参数 `num`;如果有多于一个参数,可以使用逗号分隔,比如 `|param1, param2|`。 -参数之后是存放闭包体的大括号。如果闭包体只有一行则大括号是可以省略的。大括号之后,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号,正如函数体中的一样。 +参数之后是存放闭包体的大括号 ———— 如果闭包体只有一行则大括号是可以省略的。大括号之后闭包的结尾,需要用于 `let` 语句的分号。闭包体的最后一行(`num`)返回的值将是调用闭包时返回的值,因为最后一行没有分号;正如函数体中的一样。 注意这个 `let` 语句意味着 `expensive_closure` 包含一个匿名函数的 **定义**,不是调用匿名函数的 **返回值**。回忆一下使用闭包的原因是我们需要在一个位置定义代码,储存代码,并在之后的位置实际调用它;期望调用的代码现在储存在 `expensive_closure` 中。 -现在我们定义了闭包,可以改变 `if` 块中的代码来调用闭包以执行代码并获取结果值。调用闭包看起来非常类似调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号,如示例 13-6 所示: +定义了闭包之后,可以改变 `if` 块中的代码来调用闭包以执行代码并获取结果值。调用闭包类似于调用函数;指定存放闭包定义的变量名并后跟包含期望使用的参数的括号,如示例 13-6 所示: 文件名: src/main.rs @@ -179,7 +190,7 @@ let expensive_closure = |num| { # use std::thread; # use std::time::Duration; # -fn generate_workout(intensity: i32, random_number: i32) { +fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); @@ -202,7 +213,7 @@ fn generate_workout(intensity: i32, random_number: i32) { println!( "Today, run for {} minutes!", expensive_closure(intensity) - ) + ); } } } @@ -210,15 +221,17 @@ fn generate_workout(intensity: i32, random_number: i32) { 示例 13-6:调用定义的 `expensive_closure` -现在我们达成了将慢计算统一到一个地方的目标,并只会在需要结果的时候执行改代码。然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 +现在耗时的计算只在一个地方被调用,并只会在需要结果的时候执行改代码。 + +然而,我们又重新引入了示例 13-3 中的问题:仍然在第一个 `if` 块中调用了闭包两次,这会调用慢计算两次并使用户多等待一倍的时间。可以通过在 `if` 块中创建一个本地变量存放闭包调用的结果来解决这个问题,不过正因为使用了闭包还有另一个解决方案。稍后会回到这个方案上;首先讨论一下为何闭包定义中和所涉及的 trait 中没有类型注解。 ### 闭包类型推断和注解 -闭包与由 `fn` 关键字定义的函数有一些区别。第一是不要求像 `fn` 函数那样在参数和返回值上注明类型。 +闭包不要求像 `fn` 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。 -函数中需要类型注解是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。 +另外,闭包通常很短并只与对应相对任意的场景较小的上下文中。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。 -另外,闭包通常很短并只与对应相对任意的场景较小的上下文中。在这些有限制的上下文中,编译器能可靠的推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样。强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。 +强制在这些小的匿名函数中注明类型是很恼人的,并且与编译器已知的信息存在大量的重复。 类似于变量,如果相比严格的必要性你更希望增加明确性并变得更啰嗦,可以选择增加类型注解;为示例 13-4 中定义的闭包标注类型将看起来像示例 13-7 中的定义: @@ -228,7 +241,7 @@ fn generate_workout(intensity: i32, random_number: i32) { # use std::thread; # use std::time::Duration; # -let expensive_closure = |num: i32| -> i32 { +let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num @@ -237,18 +250,18 @@ let expensive_closure = |num: i32| -> i32 { 示例 13-7:为闭包的参数和返回值增加可选的类型注解 -有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法: +有了类型注解闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的纵向对比。这里增加了一些空格来对齐相应部分。这展示了闭包语法如何类似于函数语法,除了使用竖线而不是括号以及几个可选的语法之外: ```rust,ignore -fn add_one_v1 (x: i32) -> i32 { x + 1 } -let add_one_v2 = |x: i32| -> i32 { x + 1 }; +fn add_one_v1 (x: u32) -> u32 { x + 1 } +let add_one_v2 = |x: u32| -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ; ``` -第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。 +第一行展示了一个函数定义,而第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一行。这些都是有效的闭包定义,并在调用时产生相同的行为。 -闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `i32`,则会得到一个错误: +闭包定义会为每个参数和返回值推断一个具体类型。例如,示例 13-8 中展示了仅仅将参数作为返回值的简短的闭包定义。除了作为示例的目的这个闭包并不是很实用。注意其定义并没有增加任何类型注解:如果尝试调用闭包两次,第一次使用 `String` 类型作为参数而第二次使用 `u32`,则会得到一个错误: 文件名: src/main.rs @@ -279,15 +292,15 @@ error[E0308]: mismatched types ### 使用带有泛型和 `Fn` trait 的闭包 -回到我们的健身计划生成 app ,在示例 13-6 中的代码仍然调用了多于需要的慢计算闭包。在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。 +回到我们的健身计划生成 app ,在示例 13-6 中的代码仍然调用了多于需要的慢计算闭包。解决这个问题的一个方法是在全部代码中的每一个需要多个慢计算闭包结果的地方,可以将结果保存进变量以供复用,这样就可以使用变量而不是再次调用闭包。但是这样就会有很多重复的保存结果变量的地方。 -然而,因为拥有一个慢计算的闭包,我们还可以采取另一个解决方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation*。 +幸运的是,还有另一个可用的方案。可以创建一个存放闭包和调用闭包结果的结构体。该结构体只会在需要结果时执行闭包,并会缓存结果值,这样余下的代码就不必再负责保存结果并可以复用该值。你可能见过这种模式被称 *memoization* 或 *lazy evaluation*。 -为了让结构体存放闭包,我们需要能够指定闭包的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 +为了让结构体存放闭包,我们需要能够指定闭包的类型,因为结构体定义需要知道其每一个字段的类型。每一个闭包实例有其自己独有的匿名类型:也就是说,即便两个闭包有着相同的签名,他们的类型仍然可以被认为是不同。为了定义使用闭包的结构体、枚举或函数参数,需要像第十章讨论的那样使用泛型和 trait bound。 `Fn` 系列 trait 由标准库提供。所有的闭包都实现了 trait `Fn`、`FnMut` 或 `FnOnce` 中的一个。在下一部分捕获环境部分我们会讨论这些 trait 的区别;在这个例子中可以使用 `Fn` trait。 -为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `i32` 的参数并返回一个 `i32`,这样所指定的 trait bound 就是 `Fn(i32) -> i32`。 +为了满足 `Fn` trait bound 我们增加了代表闭包所必须的参数和返回值类型的类型。在这个例子中,闭包有一个 `u32` 的参数并返回一个 `u32`,这样所指定的 trait bound 就是 `Fn(u32) -> u32`。 示例 13-9 展示了存放了闭包和一个 Option 结果值的 `Cacher` 结构体的定义: @@ -295,16 +308,18 @@ error[E0308]: mismatched types ```rust struct Cacher - where T: Fn(i32) -> i32 + where T: Fn(u32) -> u32 { calculation: T, - value: Option, + value: Option, } ``` 示例 13-9:定义一个 `Cacher` 结构体来在 `calculation` 中存放闭包并在 `value` 中存放 Option 值 -结构体 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `i32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `i32`(由 `->` 之后的内容)。 +结构体 `Cacher` 有一个泛型 `T` 的字段 `calculation`。`T` 的 trait bound 指定了 `T` 是一个使用 `Fn` 的闭包。任何我们希望储存到 `Cacher` 实例的 `calculation` 字段的闭包必须有一个 `u32` 参数(由 `Fn` 之后的括号的内容指定)并必须返回一个 `u32`(由 `->` 之后的内容)。 + +> 注意:函数也都实现了这三个 `Fn` trait。如果不需要捕获环境中的值,则在需要实现 `Fn` trait 是可以使用函数而不是闭包。 `value` 是 `Option` 类型的。在执行闭包之前,`value` 将是 `None`。如果使用 `Cacher` 的代码请求闭包的结果,这时会执行闭包并将结果储存在 `value` 字段的 `Some` 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 `Some` 成员中的结果。 @@ -314,14 +329,14 @@ struct Cacher ```rust # struct Cacher -# where T: Fn(i32) -> i32 +# where T: Fn(u32) -> u32 # { # calculation: T, -# value: Option, +# value: Option, # } # impl Cacher - where T: Fn(i32) -> i32 + where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher { Cacher { @@ -330,7 +345,7 @@ impl Cacher } } - fn value(&mut self, arg: i32) -> i32 { + fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { @@ -343,9 +358,11 @@ impl Cacher } ``` -示例 13-10:一个 `Cacher` 的关联函数 `new` 和管理缓存逻辑的 `value` 方法的实现 +示例 13-10:`Cacher` 的缓存逻辑 -`Cacher` 结构体的字段是私有的,因为我们希望 `Cacher` 管理这些值而不是任由调用代码潜在的直接改变他们。`Cacher::new` 函数获取一个泛型参数 `T`,它定义于 `impl` 块上下文中并与 `Cacher` 结构体有着相同的 trait bound。`Cacher::new` 返回一个在 `calculation` 字段中存放了指定闭包和在 `value` 字段中存放了 `None` 值的 `Cacher` 实例,因为我们还未执行闭包。 +`Cacher` 结构体的字段是私有的,因为我们希望 `Cacher` 管理这些值而不是任由调用代码潜在的直接改变他们。 + +`Cacher::new` 函数获取一个泛型参数 `T`,它定义于 `impl` 块上下文中并与 `Cacher` 结构体有着相同的 trait bound。`Cacher::new` 返回一个在 `calculation` 字段中存放了指定闭包和在 `value` 字段中存放了 `None` 值的 `Cacher` 实例,因为我们还未执行闭包。 当调用代码需要闭包的执行结果时,不同于直接调用闭包,它会调用 `value` 方法。这个方法会检查 `self.value` 是否已经有了一个 `Some` 的结果值;如果有,它返回 `Some` 中的值并不会再次执行闭包。 @@ -360,14 +377,14 @@ impl Cacher # use std::time::Duration; # # struct Cacher -# where T: Fn(i32) -> i32 +# where T: Fn(u32) -> u32 # { # calculation: T, -# value: Option, +# value: Option, # } # # impl Cacher -# where T: Fn(i32) -> i32 +# where T: Fn(u32) -> u32 # { # fn new(calculation: T) -> Cacher { # Cacher { @@ -376,7 +393,7 @@ impl Cacher # } # } # -# fn value(&mut self, arg: i32) -> i32 { +# fn value(&mut self, arg: u32) -> u32 { # match self.value { # Some(v) => v, # None => { @@ -388,7 +405,7 @@ impl Cacher # } # } # -fn generate_workout(intensity: i32, random_number: i32) { +fn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); @@ -411,7 +428,7 @@ fn generate_workout(intensity: i32, random_number: i32) { println!( "Today, run for {} minutes!", expensive_result.value(intensity) - ) + ); } } } @@ -419,9 +436,13 @@ fn generate_workout(intensity: i32, random_number: i32) { 示例 13-11:在 `generate_workout` 函数中利用 `Cacher` 结构体来抽象出缓存逻辑 -不同于直接将闭包保存进一个变量,我们保存一个新的 `Cacher` 实例来存放闭包。接着,在每一个需要结果的地方,调用 `Cacher` 实例的 `value` 方法。可以调用 `value` 方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。尝试使用示例 13-2 中的 `main` 函数来运行这段程序,并将 `simulated_user_specified_value` 和 `simulated_random_number` 变量中的值来验证在所有情况下在多个 `if` 和 `else` 块中,闭包打印的 `calculating slowly...` 只会在需要时出现并只会出现一次。 +不同于直接将闭包保存进一个变量,我们保存一个新的 `Cacher` 实例来存放闭包。接着,在每一个需要结果的地方,调用 `Cacher` 实例的 `value` 方法。可以调用 `value` 方法任意多次,或者一次也不调用,而慢计算最多只会运行一次。 -`Cacher` 负责确保不会调用超过所需的慢计算所需的逻辑,这样 `generate_workout` 就可以专注业务逻辑了。值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。 +尝试使用示例 13-2 中的 `main` 函数来运行这段程序,并改变 `simulated_user_specified_value` 和 `simulated_random_number` 变量中的值来验证在所有情况下在多个 `if` 和 `else` 块中,闭包打印的 `calculating slowly...` 只会在需要时出现并只会出现一次。`Cacher` 负责确保不会调用超过所需的慢计算所需的逻辑,这样 `generate_workout` 就可以专注业务逻辑了。 + +### `Cacher` 实现的限制 + +值缓存是一种更加广泛的实用行为,我们可能希望在代码中的其他闭包中也使用他们。然而,目前 `Cacher` 的实现存在一些小问题,这使得在不同上下文中复用变得很困难。 第一个问题是 `Cacher` 实例假设对于 `value` 方法的任何 `arg` 参数值总是会返回相同的值。也就是说,这个 `Cacher` 的测试会失败: @@ -442,19 +463,20 @@ fn call_with_different_values() { 使用示例 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 +thread 'call_with_different_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` 存放一个哈希 map 而不是单独一个值。哈希 map 的 key 将是传递进来的 `arg` 值,而 value 则是对应 key 调用闭包的结果值。相比之前检查 `self.value` 直接是 `Some` 还是 `None` 值,现在 `value` 会在哈希 map 中寻找 `arg`,如果存在就返回它。如果不存在,`Cacher` 会调用闭包并将结果值保存在哈希 map 对应 `arg` 值的位置。 -当前 `Cacher` 实现的另一个问题是它的应用被限制为只接受获取一个 `i32` 值并返回一个 `i32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 +当前 `Cacher` 实现的另一个问题是它的应用被限制为只接受获取一个 `u32` 值并返回一个 `u32` 值的闭包。比如说,我们可能需要能够缓存一个获取字符串 slice 并返回 `usize` 值的闭包的结果。请尝试引入更多泛型参数来增加 `Cacher` 功能的灵活性。 ### 闭包会捕获其环境 -在健身计划生成器的例子中,我们只将闭包作为内联匿名函数来使用。不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问定义他们的作用域的变量。 +在健身计划生成器的例子中,我们只将闭包作为内联匿名函数来使用。不过闭包还有另一个函数所没有的功能:他们可以捕获其环境并访问其被定义的作用域的变量。 示例 13-12 有一个储存在 `equal_to_x` 变量中闭包的例子,它使用了闭包环境中的变量 `x`: @@ -476,7 +498,7 @@ fn main() { 这里,即便 `x` 并不是 `equal_to_x` 的一个参数,`equal_to_x` 闭包也被允许使用变量 `x`,因为它与 `equal_to_x` 定义于相同的作用域。 -函数则不能做到同样的事,让我们试试会发生什么: +函数则不能做到同样的事,如果尝试如下例子,它并不能编译: 文件名: src/main.rs @@ -495,9 +517,9 @@ fn main() { 这会得到一个错误: ```text -error[E0434]: can't capture dynamic environment in a fn item; use the || { ... } -closure form instead - --> +error[E0434]: can't capture dynamic environment in a fn item; use the || { ... +} closure form instead + --> src/main.rs | 4 | fn equal_to_x(z: i32) -> bool { z == x } | ^ @@ -509,13 +531,15 @@ closure form instead 闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,不可变借用和可变借用。这三种捕获值的方式被编码为如下三个 `Fn` trait: -* `FnOnce` 消费从周围作用域捕获的变量(闭包周围的作用域被称为其 **环境**,*environment*)。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 `Once` 部分是因为闭包不能多次获取相同变量的所有权,所以它只能被调用一次。 +* `FnOnce` 消费从周围作用域捕获的变量,闭包周围的作用域被称为其 **环境**,*environment*。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的 `Once` 部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。 * `Fn` 从其环境不可变的借用值 * `FnMut` 可变的借用值所以可以改变其环境 当创建一个闭包时,Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。在示例 13-12 中,`equal_to_x` 闭包不可变的借用了 `x`(所以 `equal_to_x` 使用 `Fn` trait),因为闭包体只需要读取 `x` 的值。 -如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动: +如果我们希望强制闭包获取其使用的环境值的所有权,可以在参数列表前使用 `move` 关键字。这在将闭包传递给新线程以便将数据移动到新线程中时最为实用。 + +第十六章讨论并发时会展示更多 `move` 闭包的例子,不过现在这里修改了示例 13-12 中的代码(作为演示),在闭包定义中增加 `move` 关键字并使用 vector 代替整型,因为整型可以被拷贝而不是移动;注意这些代码还不能编译: 文件名: src/main.rs @@ -546,11 +570,11 @@ error[E0382]: use of moved value: `x` | ^ value used here after move | = note: move occurs because `x` has type `std::vec::Vec`, which does not - implement the `Copy` trait + implement the `Copy` trait ``` -`x` 被移动进了闭包,因为闭包使用 `move` 关键字定义。接着闭包获取了 `x` 的所有权,同时 `main` 就不再允许使用它了。去掉 `println!` 即可修复问题。 +`x` 被移动进了闭包,因为闭包使用 `move` 关键字定义。接着闭包获取了 `x` 的所有权,同时 `main` 就不再允许在 `println!` 语句中使用 `x` 了。去掉 `println!` 即可修复问题。 -大部分需要指定一个 `Fn` trait bound 的时候,可以从 `Fn` 开始,编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce`。 +大部分需要指定一个 `Fn` trait bound 的时候,可以从 `Fn` 开始,而编译器会根据闭包体中的情况告诉你是否需要 `FnMut` 或 `FnOnce`。 为了展示闭包作为函数参数时捕获其环境的作用,让我们移动到下一个主题:迭代器。