You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
trpl-zh-cn/src/ch13-03-improving-our-io-pr...

7.5 KiB

改进 I/O 项目

ch13-03-improving-our-io-project.md
commit 2cd1b5593d26dc6a03c20f8619187ad4b2485552

掌握了这些关于迭代器的新知识后,我们可以使用迭代器来改进第十二章中 I/O 项目的实现来使得代码更简洁明了。接下来,让我们看看迭代器如何改进 Config::build 函数和 search 函数的实现。

使用迭代器去除 clone

在示例 12-6 中,我们增加了一些代码获取一个 String 类型的 slice 并创建一个 Config 结构体的实例,它们索引 slice 中的值并克隆这些值以便 Config 结构体可以拥有这些值。在示例 13-17 中重现了第十二章结尾示例 12-23 中 Config::build 函数的实现:

文件名src/lib.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-12-23-reproduced/src/lib.rs:ch13}}

示例 13-17重现示例 12-23 的 Config::build 函数

当时我们说过不必担心低效的 clone 调用,因为我们以后会将其移除。好吧,就是现在!

起初这里需要 clone 的原因是参数 args 中有一个 String 元素的 slicebuild 函数并不拥有 args。为了能够返回 Config 实例的所有权,我们需要克隆 Config 中字段 queryfile_path 的值,这样 Config 实例就能拥有这些值。

在学习了迭代器之后,我们可以将 build 函数改为获取一个有所有权的迭代器作为参数,而不是借用 slice。我们将使用迭代器功能代替之前检查 slice 长度和索引特定位置的代码。这样可以更清晰地表达 Config::build 函数的操作,因为迭代器会负责访问这些值。

一旦 Config::build 获取了迭代器的所有权并不再使用借用的索引操作,就可以将迭代器中的 String 值移动到 Config 中,而不是调用 clone 分配新的空间。

直接使用返回的迭代器

打开 I/O 项目的 src/main.rs 文件,它看起来应该像这样:

文件名src/main.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-12-24-reproduced/src/main.rs:ch13}}

首先我们修改第十二章结尾示例 12-24 中的 main 函数的开头为示例 13-18 中的代码。在更新 Config::build 之前这些代码还不能编译:

文件名src/main.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-13-18/src/main.rs:here}}

示例 13-25env::args 的返回值传递给 Config::build

env::args 函数返回一个迭代器!不同于将迭代器的值收集到一个 vector 中接着传递一个 slice 给 Config::build,现在我们直接将 env::args 返回的迭代器的所有权传递给 Config::build

接下来需要更新 Config::build 的定义。在 I/O 项目的 src/lib.rs 中,将 Config::build 的签名改为如示例 13-19 所示。这仍然不能编译因为我们还需更新函数体。

文件名src/lib.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-13-19/src/lib.rs:here}}

示例 13-19以迭代器作为参数更新 Config::build 的签名

env::args 函数的标准库文档显示,它返回的迭代器的类型为 std::env::Args,并且这个类型实现了 Iterator trait 并返回 String 值。

我们已经更新了 Config::build 函数的签名,因此参数 args 有一个带有 trait bounds impl Iterator<Item = String> 的泛型类型,而不是 &[String]。这里用到了第十章 “trait 作为参数” 部分讨论过的 impl Trait 语法,这意味着 args 可以是任何实现了 Iterator trait 并返回 Stringitem的类型。

由于我们获取了 args 的所有权,并且将通过迭代来修改 args,因此我们可以在 args 参数的声明中添加 mut 关键字,使其可变。

使用 Iterator trait 代替索引

接下来,我们将修改 Config::build 的函数体。因为 args 实现了 Iterator trait因此我们知道可以对其调用 next 方法!示例 13-20 更新了示例 12-23 中的代码,以使用 next 方法:

文件名src/lib.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-13-20/src/lib.rs:here}}

示例 13-20修改 Config::build 的函数体来使用迭代器方法

请记住 env::args 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以首先调用 next 且不对其返回值做任何操作。然后,我们再次调用 next 来获取要放入 Config 结构体的 query 字段的值。如果 next 返回 Some,使用 match 来提取其值。如果它返回 None,则意味着没有提供足够的参数并通过 Err 值提早返回。我们对对 file_path 的值也进行同样的操作。

使用迭代器适配器来使代码更简明

I/O 项目中其他可以利用迭代器的地方是 search 函数,示例 13-21 中重现了第十二章结尾示例 12-19 中此函数的定义:

文件名src/lib.rs

{{#rustdoc_include ../listings/ch12-an-io-project/listing-12-19/src/lib.rs:ch13}}

示例 13-21示例 12-19 中 search 函数的定义

可以通过使用迭代器适配器方法来编写更简明的代码。这样做还可以避免使用一个可变的中间 results vector。函数式编程风格倾向于最小化可变状态的数量来使代码更简洁。去除可变状态可能会使未来的并行搜索优化变得更容易因为我们不必管理对 results vector 的并发访问。示例 13-22 展示了这一变化:

文件名src/lib.rs

{{#rustdoc_include ../listings/ch13-functional-features/listing-13-22/src/lib.rs:here}}

示例 13-22search 函数实现中使用迭代器适配器

回忆一下,search 函数的目的是返回所有 contents 中包含 query 的行。类似于示例 13-16 中的 filter 例子,这段代码使用 filter 适配器来保留 line.contains(query) 返回 true 的行。接着使用 collect 将匹配行收集到另一个 vector 中。这样就容易多了!尝试对 search_case_insensitive 函数做出同样的使用迭代器方法的修改吧。

选择循环或迭代器

接下来的逻辑问题就是在代码中应该选择哪种风格,以及原因:是使用示例 13-21 中的原始实现还是使用示例 13-22 中使用迭代器的版本?大部分 Rust 程序员倾向于使用迭代器风格。开始这有点难以掌握,不过一旦你对不同迭代器的工作方式有了感觉之后,迭代器反而更容易理解。相比摆弄不同的循环并创建新 vector迭代器代码则更关注循环的高层次目的。这抽象掉那些老生常谈的代码这样就更容易看清代码所特有的概念比如迭代器中每个元素必须满足的过滤条件。

不过这两种实现真的完全等价吗?直觉上的假设是更底层的循环会更快一些。让我们聊聊性能吧。