改进我们的I/O项目

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

使用迭代器移除 clone

在清单 12-6 中,我们添加了代码,该代码获取 String 值的切片,并通过索引切片和克隆值来创建 Config 结构体的实例,从而使 Config 结构体拥有这些值。在清单 13-17 中,我们重现了 Config::build 函数的实现,该实现与清单 12-23 中的实现相同:

Filename: 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)
        );
    }
}
Listing 13-17: Reproduction of the Config::build function from Listing 12-23

当时,我们说不要担心低效的clone调用,因为我们会在未来移除它们。好吧,现在就是那个时候了!

我们需要在这里使用clone,因为我们有一个包含String元素的切片在参数args中,但build函数并不拥有args。为了返回一个Config实例的所有权,我们必须从Configqueryfile_path字段中克隆值,以便Config实例可以拥有其值。

利用我们对迭代器的新了解,我们可以将build函数改为接受一个迭代器的所有权作为其参数,而不是借用一个切片。 我们将使用迭代器的功能,而不是检查切片长度和索引到特定位置的代码。这将使Config::build函数的作用更加清晰,因为迭代器将访问这些值。

一旦 Config::build 拥有迭代器并停止使用借用的索引操作,我们可以将 String 值从迭代器移动到 Config,而不是调用 clone 进行新的分配。

Using the Returned Iterator Directly

打开您的 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 之前不会编译。

Filename: 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);
    }
}
Listing 13-18: Passing the return value of env::args to Config::build

env::args 函数返回一个迭代器!现在,我们直接将 env::args 返回的迭代器的所有权传递给 Config::build,而不是将迭代器值收集到向量中然后再传递切片给 Config::build

接下来,我们需要更新 Config::build 的定义。在你的 I/O 项目的 src/lib.rs 文件中,让我们将 Config::build 的签名更改为如清单 13-19 所示。这仍然无法编译,因为我们需要更新函数体。

Filename: 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)
        );
    }
}
Listing 13-19: Updating the signature of Config::build to expect an iterator

标准库文档中关于env::args函数的说明显示,它返回的迭代器类型为std::env::Args,该类型实现了Iterator特征并返回String值。

我们已经更新了 Config::build 函数的签名,使参数 args 具有带有特质边界 impl Iterator<Item = String> 的泛型类型,而不是 &[String]。这种使用 impl Trait 语法的方式我们在第十章的 “特质作为参数” 部分讨论过,意味着 args 可以是任何实现了 Iterator 特质并返回 String 项的类型。

因为我们将拥有 args 并且通过迭代 args 来对其进行修改,我们可以在 args 参数的定义中添加 mut 关键字使其可变。

Using Iterator Trait Methods Instead of Indexing

接下来,我们将修复 Config::build 的主体。因为 args 实现了 Iterator 特性,我们知道可以调用它的 next 方法!清单 13-20 更新了来自清单 12-23 的代码,以使用 next 方法:

Filename: 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)
        );
    }
}
Listing 13-20: Changing the body of Config::build to use iterator methods

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

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

我们还可以在 I/O 项目中的 search 函数中利用迭代器,该函数在列表 13-21 中重现,与列表 12-19 中的一样:

Filename: 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));
    }
}
Listing 13-21: The implementation of the search function from Listing 12-19

我们可以使用迭代器适配器方法以更简洁的方式编写此代码。 这样做还可以让我们避免使用可变的中间 results 向量。函数式编程风格倾向于最小化可变状态的数量, 以使代码更清晰。移除可变状态可能使未来的增强能够并行进行搜索,因为我们将不必管理对 results 向量的并发访问。列表 13-22 显示了这一更改:

Filename: 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)
        );
    }
}
Listing 13-22: Using iterator adapter methods in the implementation of the search function

回忆一下,search 函数的目的是返回 contents 中包含 query 的所有行。类似于列表 13-16 中的 filter 示例,此代码使用 filter 适配器仅保留 line.contains(query) 返回 true 的行。然后我们使用 collect 将匹配的行收集到另一个向量中。简单多了!也可以自由地对 search_case_insensitive 函数进行相同的更改,使用迭代器方法。

选择循环或迭代器

下一个合乎逻辑的问题是,在你自己的代码中应该选择哪种风格以及为什么:是列表 13-21 中的原始实现,还是列表 13-22 中使用迭代器的版本。大多数 Rust 程序员更喜欢使用迭代器风格。一开始可能有点难以掌握,但一旦你熟悉了各种迭代器适配器及其功能,迭代器就更容易理解了。代码不再纠结于循环的各种细节和构建新向量,而是专注于循环的高层次目标。这抽象掉了一些常见的代码,使得更容易看到此代码独有的概念,例如迭代器中每个元素必须通过的过滤条件。

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