改进我们的 I/O 项目

有了关于迭代器的新知识,我们可以通过使用迭代器使代码中的某些部分更清晰、更简洁,来改进第 12 章中的 I/O 项目。让我们看看迭代器如何改进 Config::build 函数和 search 函数的实现。

使用迭代器移除 clone

在 Listing 12-6 中,我们添加了代码,它接收一个 String 值切片,并通过索引到该切片并克隆这些值来创建 Config 结构的实例,从而使 Config 结构拥有这些值。在 Listing 13-17 中,我们重现了 Config::build 函数在 Listing 12-23 中的实现方式

文件名: src/lib.rs
use std::env; use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
Listing 13-17: Listing 12-23 中 Config::build 函数的重现

当时,我们说过不必担心效率低下的 clone 调用,因为我们将来会移除它们。 好吧,现在是时候了!

我们需要这里的 clone,因为我们在参数 args 中有一个 String 元素的切片,但是 build 函数不拥有 args。 为了返回 Config 实例的所有权,我们必须从 Configqueryfile_path 字段中克隆这些值,以便 Config 实例可以拥有其值。

凭借我们关于迭代器的新知识,我们可以更改 build 函数,使其接收迭代器的所有权作为其参数,而不是借用切片。 我们将使用迭代器功能,而不是检查切片长度并索引到特定位置的代码。 这将阐明 Config::build 函数正在做什么,因为迭代器将访问这些值。

一旦 Config::build 获得迭代器的所有权并停止使用借用的索引操作,我们就可以将 String 值从迭代器移动到 Config 中,而不是调用 clone 并进行新的分配。

直接使用返回的迭代器

打开您的 I/O 项目的 *src/main.rs* 文件,它应该如下所示

文件名: src/main.rs

use std::env; use std::process; use minigrep::Config; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } }

我们将首先将 Listing 12-24 中的 main 函数的开头更改为 Listing 13-18 中的代码,这次使用了迭代器。 在我们更新 Config::build 之前,这不会编译。

文件名: src/main.rs
use std::env; use std::process; use minigrep::Config; fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } }
Listing 13-18: 将 env::args 的返回值传递给 Config::build

env::args 函数返回一个迭代器! 现在我们是将 env::args 返回的迭代器的所有权直接传递给 Config::build,而不是将迭代器值收集到一个向量中,然后将切片传递给 Config::build

接下来,我们需要更新 Config::build 的定义。 在你的 I/O 项目的 *src/lib.rs* 文件中,让我们更改 Config::build 的签名,使其看起来像 Listing 13-19。 这仍然不会编译,因为我们需要更新函数体。

文件名: src/lib.rs
use std::env; use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { // --snip-- if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
Listing 13-19: 更新 Config::build 的签名以期望迭代器

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

我们已经更新了 Config::build 函数的签名,以便参数 args 具有带有特性边界 impl Iterator<Item = String> 的泛型类型,而不是 &[String]。 我们在第 10 章的 “作为参数的特性”节中讨论的 impl Trait 语法的这种用法意味着 args 可以是任何实现 Iterator 特性并返回 String 项的类型。

因为我们要获取 args 的所有权,并且我们将通过迭代来修改 args,所以我们可以将 mut 关键字添加到 args 参数的规范中,使其可变。

使用 Iterator 特性方法代替索引

接下来,我们将修复 Config::build 的主体。 因为 args 实现了 Iterator 特性,我们知道可以在其上调用 next 方法! Listing 13-20 更新了 Listing 12-23 中的代码以使用 next 方法

文件名: src/lib.rs
use std::env; use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err("Didn't get a query string"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err("Didn't get a file path"), }; let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
Listing 13-20: 更改 Config::build 的主体以使用迭代器方法

请记住,env::args 返回值中的第一个值是程序的名称。 我们想忽略它并获取下一个值,因此首先我们调用 next 并且不使用返回值进行任何操作。 其次,我们调用 next 来获取我们要放入 Configquery 字段中的值。 如果 next 返回 Some,我们使用 match 来提取该值。 如果它返回 None,则意味着没有给出足够的参数,并且我们提前返回一个 Err 值。 我们对 file_path 值执行相同的操作。

使用迭代器适配器使代码更清晰

我们还可以在 I/O 项目中的 search 函数中利用迭代器,该函数在 Listing 13-21 中重现,因为它在 Listing 12-19 中

文件名: src/lib.rs
use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, } impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } #[cfg(test)] mod tests { use super::*; #[test] fn one_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } }
Listing 13-21: Listing 12-19 中 search 函数的实现

我们可以使用迭代器适配器方法以更简洁的方式编写此代码。 这样做还可以使我们避免使用可变中间 results 向量。 函数式编程风格倾向于最小化可变状态量,以使代码更清晰。 删除可变状态可能会使将来的增强功能能够并行进行搜索,因为我们不必管理对 results 向量的并发访问。 Listing 13-22 展示了这种变化

文件名: src/lib.rs
use std::env; use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err("Didn't get a query string"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err("Didn't get a file path"), }; let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect() } pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
Listing 13-22: 在 search 函数的实现中使用迭代器适配器方法

回想一下,search 函数的目的是返回 contents 中包含 query 的所有行。 与 Listing 13-16 中的 filter 示例类似,此代码使用 filter 适配器来仅保留那些 line.contains(query) 返回 true 的行。 然后,我们使用 collect 将匹配的行收集到另一个向量中。 简单得多! 随意进行相同的更改,以在 search_case_insensitive 函数中也使用迭代器方法。

在循环或迭代器之间选择

下一个合乎逻辑的问题是您应该在自己的代码中选择哪种风格以及为什么: Listing 13-21 中的原始实现还是 Listing 13-22 中使用迭代器的版本。 大多数 Rust 程序员更喜欢使用迭代器风格。 一开始上手有点困难,但是一旦您对各种迭代器适配器及其功能有所了解,迭代器就会更容易理解。 该代码不是摆弄各种循环和构建新向量的位,而是专注于循环的高级目标。 这抽象掉了一些常见的代码,因此更容易看到此代码特有的概念,例如迭代器中每个元素必须通过的过滤条件。

但是,这两种实现真的等效吗? 直观的假设可能是较低级别的循环会更快。 让我们谈谈性能。