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