改进我们的 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 样式。一开始要掌握窍门有点困难,但是一旦您了解了各种迭代器适配器及其功能,迭代器就会更容易理解。该代码不是摆弄各种循环和构建新向量,而是专注于循环的高级目标。这将抽象出一些常见的代码,以便更容易地看到此代码独有的概念,例如迭代器中的每个元素必须传递的筛选条件。


但是这两种实现真的等价吗?直观的假设可能是,低级循环越多,速度越快。我们来谈谈性能。