学习了关于迭代器的知识之后,我们可以对之前的输入/输出项目做一些改进,是的代码变得更加简洁明了。接下来让我们看一下如何在Config::build和search函数中使用迭代器做出改进。
15.3.1 使用迭代器替换clone方法
查看下面的代码,我们可以看到我们使用了字符串数组索引值来获取对应的数组的切片,然后复制一份保存到本地变量,然后生成Config结构的实例并返回,返回的是带有所有权的结构体实例。
rust
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("文件参数不足!");
}
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 })
}
}
这次我们不在担心clone的效率问题,因为之后我们会替换掉它们,现在让我们开始吧。
我们需要clone是因为字符串元素是来自参数的一个切片,build并没有它的所有权。为了返回Config实例,不得不将切片复制一份到变量query和file_path变量中,以便Config实例可以拥有它的所有权。
我们学习了关于迭代器的新知识后,我们着手改变fuild函数,使用迭代器转移所有权而不是对切片的借用。我们使用迭代器的功能来替换掉代码中的长度检查和使用索引来获取特定位置的切片。这将使得Config::bulid方法更加清晰,因为迭代器可以直接获取该值。
一旦Config::build使用迭代器获取所有权,并停止使用索引来获取数组切片的借用,我们就可以将字符串变量从迭代器中转移到Config实例中而不是调用clone方法来重新进行分配。
打开之前的输入/输出项目,main.rs如下所示:
rust
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("解析参数出错:{err}");
process::exit(1);
});
println!("要搜索的字符串是:{}",config.query);
println!("要搜素的文件是:{}",config.file_path);
if let Err(e) =run(config){
eprintln!("错误信息是:{e}");
process::exit(1);
};
}
我们将首先修改man函数中参数的传递,之前是先获取参数数组,然后转递给build方法,现在是,直接将参数的迭代器传递给build函数:
env::args函数返回一个迭代器,而不是之前的将迭代器的结果转换为一个数组然后将数组借用给build函数。接下来需要修改build函数签名的代码:
rust
fn build(mut args:impl Iterator<Item=String>) -> Result<Config, &'static str> {
标注库对env::args的说明是它返回的是std::env::Args的迭代器类型,这个类型实现了Iterator接口并返回字符串的值。
我们修改了fuild函数签名使其符合参数args的返回类型。这个参数类型是一个泛型约束规范------impl Iterator<Item=String>,替换掉之前的&String。impl Trait的语法指明args是任何实现了Iterator接口并返回字符串的任意类型。
因为需要占据参数args的所有权,我们必须将args指定为可变的(使用mut关键字)。
接下来修改build函数内部的代码。因为args实现了Iterator接口,我们可以调用next方法,如下所示:
rust
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("缺少参数:要搜索的字符串!"),
};
let file_path = match args.next() {
Some(path) => path,
None => return Err("缺少参数:在哪个文件中进行搜索的文件名!"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config { query, file_path,ignore_case })
}
注意args返回的第一个值是程序名,我们不需要这个值,因此忽略掉;第二个值是需要搜索的字符串,因此赋值给query变量,第三个值是要搜的文件,因此赋值给file_path。next返回的是Option枚举类型,需要使用match进行解析,如果是Some就抽取其值,如果是None就返回Err类型的值。
15.3.2 使用迭代器的适配器使代码更加清晰
我们还可以在search函数中使用迭代器,我们先看看之前使用for循环的代码,如下所示:
rust
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
}
我们可以使用迭代器进一步简化这些代码,首先取消中介result变量,这样不但可以使代码量更小,也更加清晰明了;去除代码中变量的可变的状态,同时也可以在未来并发查询的时候不用考虑冲突和一致性的问题。
rust
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
search方法的作用是查找包含query中内容的所有行,并返回。filter可以完成此功能,它使用闭包函数line.contains(query)来判断是否包含该字符串,如果包含则使用collect方法进行收集到一个向量数组中。这样是不是更加简单!也可以在search_case_insensitive方法中使用通用的代码。
如果需要返回一个迭代器,可以将最后一行.collect()删除,然后将search的返回值修改为impl Iterator<Item='&a str>;这样就会返回一个迭代器的适配器。但是你还需要更新tests的代码。如果需要观测这两个版本的区别,你需要打开一个非常大的文本文件,然后进行搜索,观测获取结果的区别。后面这个版本每当找到一行匹配的文本就会显示出来,因为在run函数中的for循环可以充分利用迭代器的懒加载的优势。
15.3.3 循环和迭代器之间何去何从
使用循环还是迭代器,我们该做出何种抉择?很多程序员更喜欢使用迭代器。也许迭代器可能没有那么容易上手,但是一旦你掌握了不同迭代器的适配器和它们是如何工作的,迭代器是更容易理解。不在需要思考循环的各种琐碎的细节和构造新的向量,代码主要关注在循环的高级目标。于是从常见的代码中脱身,专注于代码独特的理念,例如每次迭代时转递过来的每个元素的过滤条件等等。
但是这两种方式等效吗?直观的感觉是低级的循环可能更快,真的是这样吗?