改进咱们的 I/O 项目

Improving Our I/O Project

有了迭代器方面的新知识,咱们便可通过使用迭代器,改进第 12 章中的 I/O 项目,令到代码各处更清楚与简练。咱们来看看,迭代器可怎样改进其中 Config::buildsearch 函数的实现。

使用 Iterator 消除 clone

Removing a clone Using an Iterator

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

文件名:src/lib.rs

#![allow(unused)]
fn main() {
impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("参数数量不足");
        }

        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,
        })
    }
}
}

清单 13-17:清单 12-23 中 Config::build 函数的重现

当时,我们说不用担心低效的克隆调用,因为将来咱们会移除它们。好吧,现在是时候了!

咱们这里之所以需要 clone 方法,是由于在参数 args 中,咱们有一个 String 元素构成的切片,而 build 函数并不拥有 args。为返回 Config 实例的所有权,咱们不得不克隆 Config 结构体的 queryfilename 字段,进而 Config 实例便可拥有他的值。

利用我们对迭代器的新知识,我们可以改变构建函数,使其拥有一个迭代器作为其参数,而不是借用一个切片。我们将使用迭代器的功能,而不是检查切片的长度并对特定位置进行索引的代码。这将明确 Config::build 函数正在做什么,因为迭代器将访问这些值。

一旦 Config::build 取得迭代器的所有权,而不再使用借用的索引操作,咱们就可以将 String 值从迭代器迁移到 Config 中,而不是调用 clone 方法并构造新的内存分配。

直接使用返回的迭代器

Using the Returned Iterator Directly

请打开咱们 I/O 项目的 src/main.rs 文件,其看起来应是这样的:

文件名:src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln! ("解析参数时遇到问题:{err}");
        process::exit(1);
    });

    // --跳过代码--
}

咱们将首先把咱们在清单 12-24 中,有着的 main 函数开头,修改为下面清单 13-18 中,使用迭代器的代码。在咱们一并更新 Config::build 前,这不会编译。

文件名:src/main.rs

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln! ("解析参数时遇到问题:{err}");
        process::exit(1);
    });

    // --跳过代码--
}

清单 13-18:把 env::args 的返回值传递给 Config::build

env::args 函数会返回一个迭代器!相比于把迭代器值收集到矢量值,并随后把一个切片传递给 Config::build,现在咱们是在直接把由 env::args 返回的迭代器所有权,传递给 Config::build

接下来,咱们需要更新 Config::build 的定义。在咱们 I/O 项目的 src/data_structures.rs 文件中,咱们就要像下面清单 13-19 中那样,修改 Config::build 的函数签名。由于咱们需要更新该函数的主体体,因此这仍不会编译。

文件名:src/lib.rs

#![allow(unused)]
fn main() {
impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --跳过代码--
}

清单 13-19:将 Config::build 的函数签名,更新为期待得到一个迭代器

env::args 函数的标准库文档显示,其返回的迭代器类型为 std::env::Args,且那种类型实现了 Iterator 特质,并会返回 String 值。

咱们已更新了 Config::build 函数的签名,那么参数 args 就会有一个有着特质边界 impl Iterator<Item = String> 的泛型,而不再是 &[String] 类型。咱们曾在第 10 章 作为参数的特质 小节中,讨论过的 impl Trait 语法用法,表明 args 可以是任何实现了 Iterator 类型,且返回 String 条目的类型。

由于咱们正取得 args 的所有权,且咱们将通过对其迭代而修改 args,咱们便可把 mut 关键字,添加到 args 参数的说明中,以将其构造为可变。

使用 Iterator 特质的方法而非索引

Using Iterator Trait Methods Instead of Indexing

接下来,咱们将修正 Config::build 函数的主体。由于 args 实现了 Iterator 特质,咱们便清楚咱们可以调用他上面 next 方法!下面清单 13-20 将清单 12-23 中的代码,更新为了使用 next 方法:

文件名:src/data_structures.rs

#![allow(unused)]
fn main() {
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("未曾获取到查询字串"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("未曾获取到文件路径"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}
}

清单 13-20:将 Config::build 函数的主体,修改为使用迭代器方法

请记住 env::args 返回值中的第一个值,是程序的名字。咱们是要忽略那个值,而到下一值处,所以咱们首先调用 next,并对返回值不做任何处理。其次,咱们调用 next 来获取到咱们想要放入 Configquery 字段的值。若 next 返回一个 Some,咱们就使用 match 来提取该值。若其返回了 None,就意味着没有给出足够的参数,而咱们就及早地返回一个 Err 值。对于 filename 值,咱们进行了同样的处理。

使用迭代器适配器,令到代码更清晰

Making Code Clearer with Iterator Adaptors

咱们也可以在 I/O 项目的 search 函数中利用迭代器,取被转载于下面清单 13-21 中,如同其曾在清单 12-19 中那样:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
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
}
}

清单 13-21:清单 12-19 中 search 函数的实现

咱们可使用迭代器适配器的方法,以更简练方式编写出此代码。这样做还可以让我们避免有一个可变的中间 results 矢量值。函数式编程风格,the functional programming style,倾向于最小化可变状态的数量以使代码更清晰。移除可变状态,就可能让令到搜索并行进行的今后功能增强可行,因为咱们将不必管理到 results 矢量的并发访问。下面清单 13-22 给出了这一修改:

文件名:src/lib.rs

#![allow(unused)]
fn main() {
pub fn search<'a>(
    query: &str,
    contents: &'a str
) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}
}

清单 13-22:在 search 函数实现中使用迭代器适配器方法

回顾一下,search 函数的目的是要返回 contents 中包含 query 的所有行。与清单 13-16 中的 filter 示例类似,此代码使用 filter 适配器来只保留 line.contains(query) 返回 true 的行。咱们随后使用 collect(),把匹配行收集到另一矢量值中。这就简单多了!请随意做出同样的改变,在 search_case_insensitive 函数中使用迭代器方法。

函数 search_case_insenstitive 修改后如下所示:

#![allow(unused)]
fn main() {
pub fn search_insensitive<'a>(
    query: &str,
    contents: &'a str
) -> Vec<&'a str> {
    let query = query.to_lowercase();

    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}
}

在循环或迭代器之间做出选择

Choosing Between Loops or Iterators

下一个合乎逻辑的问题是,咱们应在自己的代码中选择哪种风格与为什么:清单 13-21 中原本的实现,或清单 13-22 中用到迭代器的版本。大多数 Rust 程序员喜欢使用迭代器风格。一开始他有点难掌握,但一旦咱们对各种迭代器适配器和他们的作用有了感觉,迭代器就会更容易理解。该代码没有拨弄循环的各个部分,与构建出新的矢量值,而是专注于循环的高级目标。这就把一些普通的代码抽象化了,所以更容易看到这段代码特有的概念,比如迭代器中每个元素必须通过的过滤条件。

但是,这两种实现方式真的等同吗?直观的假设可能是,更低级别的循环会更快。接下来咱们就会谈及性能问题。

Last change: 2023-11-30, commit: 400a9f9