改进我们的 I/O 项目
有了这些关于迭代器的新知识,我们可以在
第 12 章 通过使用迭代器使代码中的位置更清晰、更清晰
简明。让我们看看迭代器如何改进
Config::build
函数和 search
函数。
使用迭代器删除克隆
在示例 12-6 中,我们添加了代码,该代码获取 String
值的切片,并通过索引切片并克隆值来创建 Config
结构体的实例,从而允许 Config
结构体拥有这些值。在示例 13-17 中,我们重现了示例 12-23 中 Config::build
函数的实现:
文件名: 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)
);
}
}
示例 13-17:示例 12-23 中
Config::build
函数的重现
当时,我们说不用担心低效的克隆
调用,因为将来我们会删除它们。好吧,现在就是那个时候!
我们需要在这里克隆
,因为我们有一个在参数 args
中包含 String
元素的切片,但 build
函数不拥有 args
。要返回 Config
实例的所有权,我们必须从查询
中克隆值
file_path
Config
的字段,以便 Config
实例可以拥有其值。
凭借我们对迭代器的新知识,我们可以将 build
函数更改为
将迭代器的所有权作为其参数,而不是借用 slice。
我们将使用迭代器功能,而不是检查长度
的切片并索引到特定位置。这将阐明
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);
}
}
我们首先将示例 12-24 中的 main
函数的开头更改为示例 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);
}
}
示例 13-18:将
env::args
的返回值传递给 Config::build
env::args
函数返回一个迭代器!现在我们将从 env::args
返回的迭代器的所有权传递给 Config::build,而不是将迭代器的值收集到一个 vector 中,然后将一个 slice 传递给 Config::build
Config::build
中。
接下来,我们需要更新 Config::build
的定义。在 I/O 项目的 src/lib.rs 文件中,让我们将 Config::build
的签名更改为示例 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)
);
}
}
示例 13-19:更新
Config::build
的签名以期望一个迭代器env::args
函数的标准库文档显示它返回的迭代器的类型是 std::env::Args
,并且该类型实现了 Iterator
trait 并返回 String
值。
我们更新了 Config::build
函数的签名,因此参数
args
具有具有 trait bounds impl Iterator<Item = String>
的泛型类型
而不是 &[String]。
我们在第 10 章的 “作为参数的 trait” 部分中讨论的 impl Trait
语法的这种用法意味着 args
可以是实现 Iterator
trait 并返回 String
项的任何类型。
因为我们获得了 args
的所有权,并且我们将通过迭代来改变 args
,所以我们可以将 mut
关键字添加到
args
参数使其可变。
使用 Iterator
trait 方法而不是索引
接下来,我们将修复 Config::build
的主体。由于 args
实现了
Iterator
trait 时,我们知道我们可以在它上面调用 next
方法!示例 13-20 更新了示例 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)
);
}
}
示例 13-20:将
Config::build
的主体更改为使用迭代器方法
请记住,env::args
的返回值中的第一个值是
程序。我们想忽略它并获取下一个值,因此首先我们调用
next
不对返回值执行任何作。其次,我们调用 next
来获取我们想要放入 Config 的 query
字段中的值。如果
next
返回
有些
,我们使用 match
来提取值。如果它返回 None
,则表示没有给出足够的参数,我们提前返回 Err
值。我们对 file_path
值做同样的事情。
使用 Iterator 适配器使代码更清晰
我们还可以在 I/O 项目的搜索
函数中利用迭代器,这在示例 13-21 中重现,与示例 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));
}
}
示例 13-21:示例 12-19 中
搜索
函数的实现
我们可以使用 iterator 适配器方法以更简洁的方式编写此代码。这样做还可以避免使用可变的中间结果
向量。函数式编程风格倾向于最小化可变状态的数量,以使代码更清晰。删除可变状态可能会使未来的增强功能能够使搜索并行进行,因为我们不必管理对结果
向量的并发访问。示例 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)
);
}
}
示例 13-22:在
搜索
函数的实现中使用迭代器适配器方法
回想一下,搜索
函数的目的是返回
包含
查询
的内容。与示例 13-16 中的 filter
示例类似,此代码使用 filter
适配器仅保留
line.contains(query)
返回 true
。然后,我们使用 collect
将匹配的行收集到另一个向量中。简单多了!也可以随意进行相同的更改,以便在 search_case_insensitive
函数中使用 iterator 方法。
在循环或迭代器之间进行选择
下一个逻辑问题是你应该在自己的代码中选择哪种样式以及为什么:示例 13-21 中的原始实现或示例 13-22 中使用迭代器的版本。大多数 Rust 程序员更喜欢使用 iterator 样式。一开始要掌握窍门有点困难,但是一旦您了解了各种迭代器适配器及其功能,迭代器就会更容易理解。该代码不是摆弄各种循环和构建新向量,而是专注于循环的高级目标。这将抽象出一些常见的代码,以便更容易地看到此代码独有的概念,例如迭代器中的每个元素必须传递的筛选条件。
但是这两种实现真的等价吗?直观的假设可能是,低级循环越多,速度越快。我们来谈谈性能。