改进我们的 I/O 项目

有了这些关于迭代器的新知识,我们可以在 第 12 章 通过使用迭代器使代码中的位置更清晰、更清晰 简明。让我们看看迭代器如何改进Config::build函数和search功能。

删除clone使用 Iterator

在示例 12-6 中,我们添加了一些代码,该代码将String值并创建 的Configstruct 中,通过索引到切片中并克隆 值,允许Configstruct 来拥有这些值。在示例 13-17 中, 我们复制了Config::build按原样运作 在示例 12-23 中:

文件名: 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:复制Config::build示例 12-23 中的 function

当时,我们说不用担心效率低下clone调用 我们将来会删除它们。好吧,现在就是那个时候!

我们需要clone这里,因为我们有一个带有String元素中的 参数args,但build函数不拥有args.返回 所有权Config实例中,我们必须从queryfile_path字段Config所以Configinstance 可以拥有其值。

凭借我们对迭代器的新了解,我们可以更改build函数设置为 将迭代器的所有权作为其参数,而不是借用 slice。 我们将使用迭代器功能,而不是检查长度 的切片并索引到特定位置。这将阐明Config::build函数正在执行,因为迭代器将访问这些值。

一次Config::build获取迭代器的所有权并停止使用索引 作,我们可以将String值从 iterator 转换为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);
    }
}

我们首先将main我们在 清单 中的函数 12-24 添加到示例 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::argsConfig::build

env::argsfunction 返回一个迭代器!而不是收集 iterator 值转换为 vector,然后将 slice 传递给Config::build现在 我们正在传递从env::argsConfig::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函数显示 type 的 iterator 为std::env::Args,并且该类型实现 这Iteratortrait 和 returnsString值。

我们更新了Config::build函数,因此参数args具有具有 trait bounds 的泛型类型impl Iterator<Item = String>而不是&[String].这种impl Trait语法 第 10 章的“Traits as Parameters”部分 意味着args可以是实现Iteratortrait 和 返回String项目。

因为我们获得了args我们将进行 mutingargs由 迭代它,我们可以添加mutkeyword 添加到其args参数使其可变。

Iteratortrait 方法而不是索引

接下来,我们将修复Config::build.因为args实现Iteratortrait 中,我们知道我们可以调用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使用 Iterator 方法

请记住,返回值env::args是 的名称 程序。我们想忽略它并获取下一个值,因此首先我们调用next并且不对返回值执行任何作。其次,我们调用next要获取 值,我们想要放入query字段为Config.如果next返回Some,我们使用match以提取值。如果返回None,这意味着 没有给出足够的参数,我们提前返回Err价值。我们这样做 同样的file_path价值。

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

我们还可以利用search函数 project,在示例 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:search示例 12-19 中的 function

我们可以使用 iterator 适配器方法以更简洁的方式编写此代码。 这样做还可以避免使用可变的中间results向量。这 函数式编程风格倾向于将可变状态的数量最小化为 使代码更清晰。删除可变状态可能会启用将来的增强功能 使搜索并行进行,因为我们不必管理 并发访问results向量。示例 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:在search功能

回想一下,search函数是返回contents包含query.与filter清单中的示例 13-16 中,此代码使用filteradapter 以仅保留line.contains(query)返回true为。然后我们收集匹配的行 转换为另一个向量,其中collect.简单多了!随意做同样的事情 更改为在search_case_insensitive函数设置为 井。

在循环或迭代器之间进行选择

下一个逻辑问题是您应该在自己的代码中选择哪种样式,并且 原因:示例 13-21 中的原始实现或使用 示例 13-22 中的 iterators。大多数 Rust 程序员更喜欢使用 iterator 风格。一开始要掌握窍门有点困难,但一旦你有了感觉 对于各种迭代器适配器及其功能,迭代器可能更容易 理解。而不是摆弄各种循环和构建 new vectors,则代码侧重于循环的高级目标。这 抽象出一些常见的代码,以便更容易看到概念 对于此代码是唯一的,例如每个元素中的 iterator 必须通过。

但是这两种实现真的等价吗?直观的假设 可能是 MORE LOW Level Loop 会更快。我们来谈谈 性能。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准