使用迭代器处理一系列项目

Beginner

This tutorial is from open-source community. Access the source code

简介

欢迎来到「使用迭代器处理一系列项目」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习你的 Rust 技能。

在本实验中,我们将探索如何使用迭代器来处理一系列项目,迭代器是惰性的,允许我们在不必自己重新实现逻辑的情况下遍历一系列项目。

使用迭代器处理一系列项目

迭代器模式允许你依次对一系列项目执行某些任务。迭代器负责遍历每个项目的逻辑,并确定序列何时结束。当你使用迭代器时,无需自己重新实现该逻辑。

在 Rust 中,迭代器是「惰性的」,这意味着在你调用消耗迭代器的方法将其用完之前,它们不会产生任何效果。例如,清单 13-10 中的代码通过调用 Vec<T> 上定义的 iter 方法,创建了一个针对向量 v1 中项目的迭代器。这段代码本身并没有做任何有用的事情。

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

清单 13-10:创建一个迭代器

迭代器存储在 v1_iter 变量中。一旦我们创建了一个迭代器,就可以用多种方式使用它。在清单 3-5 中,我们使用 for 循环遍历数组,以便对其每个项目执行一些代码。在底层,这隐式地创建并消耗了一个迭代器,但到目前为止我们一直没有详细说明它具体是如何工作的。

在清单 13-11 的示例中,我们将迭代器的创建与在 for 循环中对迭代器的使用分开。当使用 v1_iter 中的迭代器调用 for 循环时,迭代器中的每个元素会在循环的一次迭代中被使用,从而打印出每个值。

let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {val}");
}

清单 13-11:在 for 循环中使用迭代器

在那些标准库中没有提供迭代器的语言中,你可能会通过从索引 0 开始一个变量,使用该变量索引到向量中获取一个值,并在循环中递增变量值,直到它达到向量中项目的总数,来编写相同的功能。

迭代器为你处理所有这些逻辑,减少了你可能会搞乱的重复代码。迭代器为你提供了更大的灵活性,可以将相同的逻辑用于许多不同类型的序列,而不仅仅是像向量这样可以通过索引访问的数据结构。让我们来看看迭代器是如何做到这一点的。

迭代器 trait 与 next 方法

所有迭代器都实现了一个在标准库中定义的名为 Iterator 的 trait。该 trait 的定义如下:

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 省略了具有默认实现的方法
}

注意,这个定义使用了一些新语法:type ItemSelf::Item,它们正在为这个 trait 定义一个「关联类型」。我们将在第 19 章深入讨论关联类型。目前,你只需要知道这段代码表明实现 Iterator trait 需要你也定义一个 Item 类型,并且这个 Item 类型用于 next 方法的返回类型。换句话说,Item 类型将是迭代器返回的类型。

Iterator trait 只要求实现者定义一个方法:next 方法,它一次返回迭代器的一个项目,包装在 Some 中,并且在迭代结束时返回 None

我们可以直接在迭代器上调用 next 方法;清单 13-12 展示了对从向量创建的迭代器重复调用 next 时返回的值。

文件名:src/lib.rs

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];

    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

清单 13-12:在迭代器上调用 next 方法

注意,我们需要使 v1_iter 可变:在迭代器上调用 next 方法会改变迭代器用于跟踪其在序列中位置的内部状态。换句话说,这段代码「消耗」或用完了迭代器。每次调用 next 都会从迭代器中消耗一个项目。当我们使用 for 循环时,不需要使 v1_iter 可变,因为循环获取了 v1_iter 的所有权并在幕后使其可变。

还要注意,我们从调用 next 中得到的值是对向量中值的不可变引用。iter 方法产生一个针对不可变引用的迭代器。如果我们想创建一个获取 v1 的所有权并返回拥有值的迭代器,我们可以调用 into_iter 而不是 iter。同样,如果我们想遍历可变引用,我们可以调用 iter_mut 而不是 iter

消耗迭代器的方法

Iterator trait 有许多由标准库提供默认实现的不同方法;你可以通过查看 Iterator trait 的标准库 API 文档来了解这些方法。其中一些方法在其定义中调用了 next 方法,这就是为什么在实现 Iterator trait 时需要实现 next 方法。

调用 next 的方法被称为「消耗适配器」,因为调用它们会用完迭代器。一个例子是 sum 方法,它获取迭代器的所有权,并通过重复调用 next 来遍历项目,从而消耗迭代器。在遍历过程中,它将每个项目加到一个运行总和中,并在迭代完成时返回总和。清单 13-13 有一个测试说明了 sum 方法的使用。

文件名:src/lib.rs

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}

清单 13-13:调用 sum 方法以获取迭代器中所有项目的总和

在调用 sum 之后,我们不能再使用 v1_iter,因为 sum 获取了我们在其上调用它的迭代器的所有权。

产生其他迭代器的方法

「迭代器适配器」是在 Iterator trait 上定义的方法,它们不会消耗迭代器。相反,它们通过改变原始迭代器的某些方面来产生不同的迭代器。

清单 13-14 展示了调用迭代器适配器方法 map 的示例,该方法在遍历项目时会对每个项目调用一个闭包。map 方法返回一个新的迭代器,该迭代器产生修改后的项目。这里的闭包创建了一个新的迭代器,其中向量中的每个项目都会增加 1。

文件名:src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1);

清单 13-14:调用迭代器适配器 map 来创建一个新的迭代器

然而,这段代码会产生一个警告:

warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

清单 13-14 中的代码没有做任何事情;我们指定的闭包从未被调用。这个警告提醒了我们原因:迭代器适配器是惰性的,我们在这里需要消耗迭代器。

为了修复这个警告并消耗迭代器,我们将使用 collect 方法,我们在清单 12-1 中对 env::args 使用过这个方法。这个方法会消耗迭代器,并将结果值收集到一个集合数据类型中。

在清单 13-15 中,我们将调用 map 返回的迭代器进行迭代的结果收集到一个向量中。这个向量最终将包含原始向量中的每个项目,每个项目都增加了 1。

文件名:src/main.rs

let v1: Vec<i32> = vec![1, 2, 3];

let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, vec![2, 3, 4]);

清单 13-15:调用 map 方法创建一个新的迭代器,然后调用 collect 方法消耗新的迭代器并创建一个向量

因为 map 接受一个闭包,所以我们可以指定对每个项目要执行的任何操作。这是闭包如何让你在重用 Iterator trait 提供的迭代行为的同时自定义某些行为的一个很好的例子。

你可以链式调用多个迭代器适配器,以一种可读的方式执行复杂的操作。但是因为所有迭代器都是惰性的,所以你必须调用其中一个消耗适配器方法才能从对迭代器适配器的调用中获得结果。

使用捕获其环境的闭包

许多迭代器适配器都将闭包作为参数,通常我们作为迭代器适配器参数指定的闭包将是捕获其环境的闭包。

对于这个示例,我们将使用接受闭包的 filter 方法。闭包从迭代器中获取一个项目并返回一个 bool。如果闭包返回 true,该值将包含在 filter 产生的迭代中。如果闭包返回 false,该值将不被包含。

在清单 13-16 中,我们将 filter 与一个从其环境中捕获 shoe_size 变量的闭包一起使用,以遍历 Shoe 结构体实例的集合。它将只返回指定尺码的鞋子。

文件名:src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

清单 13-16:将 filter 方法与捕获 shoe_size 的闭包一起使用

shoes_in_size 函数将鞋子向量的所有权和鞋子尺码作为参数。它返回一个只包含指定尺码鞋子的向量。

shoes_in_size 的主体中,我们调用 into_iter 创建一个获取向量所有权的迭代器。然后我们调用 filter 将该迭代器适配成一个新的迭代器,该迭代器只包含闭包返回 true 的元素。

闭包从环境中捕获 shoe_size 参数,并将该值与每只鞋子的尺码进行比较,只保留指定尺码的鞋子。最后,调用 collect 将适配后的迭代器返回的值收集到一个由函数返回的向量中。

测试表明,当我们调用 shoes_in_size 时,我们只会得到与我们指定的值具有相同尺码的鞋子。

总结

恭喜你!你已经完成了「使用迭代器处理一系列项目」实验。你可以在 LabEx 中练习更多实验来提升你的技能。