改进我们的 I/O 项目

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

使用迭代器移除 clone

在 Listing 12-6 中,我们添加了代码,该代码获取 String 值的切片并通过索引切片并克隆值来创建 Config 结构体的实例,从而使 Config 结构体拥有这些值。在 Listing 13-17 中,我们重现了 Listing 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)
        );
    }
}

当时,我们说不用担心低效的 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 之前,这不会编译。

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 返回的迭代器的所有权传递给 Config::build

接下来,我们需要更新 Config::build 的定义。在你的 I/O 项目的 src/lib.rs 文件中,让我们将 Config::build 的签名改为如 Listing 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)
        );
    }
}

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

我们更新了 Config::build 函数的签名,使参数 args 具有一个泛型类型,其 trait 约束为 impl Iterator<Item = String>,而不是 &[String]。我们在第 10 章的 “Traits as Parameters” 部分讨论的 impl Trait 语法的这种用法意味着 args 可以是任何实现 Iterator trait 并返回 String 项的类型。

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

使用 Iterator Trait 方法代替索引

接下来,我们将修复 Config::build 的函数体。因为 args 实现了 Iterator trait,我们知道我们可以调用它的 next 方法!Listing 13-20 更新了 Listing 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)
        );
    }
}

记住,env::args 返回值的第一个值是程序的名称。我们希望忽略它并获取下一个值,所以我们首先调用 next 并不处理返回值。然后我们调用 next 来获取我们想要放入 Configquery 字段的值。如果 next 返回 Some,我们使用 match 提取值。如果它返回 None,这意味着没有提供足够的参数,我们提前返回一个 Err 值。我们对 file_path 值做同样的事情。

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

我们还可以在我们的 I/O 项目中的 search 函数中利用迭代器,该函数在 Listing 13-21 中重现,如 Listing 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));
    }
}

我们可以使用迭代器适配器方法以更简洁的方式编写此代码。这样做还可以避免使用可变的中间 results 向量。函数式编程风格倾向于最小化可变状态的数量,以使代码更清晰。移除可变状态可能会使未来的增强功能更容易实现,例如并行搜索,因为我们不必管理对 results 向量的并发访问。Listing 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 函数的目的是返回 contents 中包含 query 的所有行。与 Listing 13-16 中的 filter 示例类似,此代码使用 filter 适配器仅保留 line.contains(query) 返回 true 的行。然后我们使用 collect 将匹配的行收集到另一个向量中。简单多了!你也可以在 search_case_insensitive 函数中做出相同的更改以使用迭代器方法。

在循环或迭代器之间选择

下一个逻辑问题是,你应该在自己的代码中选择哪种风格以及为什么:Listing 13-21 中的原始实现还是 Listing 13-22 中使用迭代器的版本。大多数 Rust 程序员更喜欢使用迭代器风格。起初有点难以掌握,但一旦你熟悉了各种迭代器适配器及其功能,迭代器可能会更容易理解。与其摆弄各种循环和构建新向量的细节,代码更关注循环的高级目标。这抽象出了一些常见的代码,因此更容易看到此代码特有的概念,例如迭代器中每个元素必须通过的过滤条件。

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