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


迭代器模式允许您依次对一系列项目执行某些任务。迭代器负责迭代每个项并确定序列何时完成的逻辑。当您使用迭代器时,您不必自己重新实现该逻辑。


在 Rust 中,迭代器是惰性的,这意味着在您调用使用迭代器的方法以使用它之前,它们没有任何影响。例如,示例 13-10 中的代码通过调用 Vec<T> 上定义的 iter 方法,在向量 v1 中的项目上创建一个迭代器。此代码本身不会执行任何有用的作。


文件名: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}

示例 13-10:创建迭代器


迭代器存储在 v1_iter 变量中。一旦我们创建了一个迭代器,我们就可以以多种方式使用它。在示例 3-5 的第 3 章中,我们使用 for 循环迭代一个数组,以在其每个项目上执行一些代码。在后台,这隐式地创建并使用了一个迭代器,但直到现在我们才掩盖了它究竟是如何工作的。


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


文件名: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

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

示例 13-11:在 for 循环中使用迭代器


在没有标准库提供的迭代器的语言中,你可能会通过以下方式编写相同的功能:在索引 0 处启动一个变量,使用该变量对向量进行索引以获取值,然后在循环中递增变量值,直到它达到向量中的项目总数。


迭代器为你处理所有这些逻辑,减少你可能会搞砸的重复代码。迭代器使您可以更灵活地将相同的 logic 用于许多不同类型的序列,而不仅仅是您可以索引的数据结构,例如 vector。让我们看看迭代器是如何做到这一点的。


Iterator trait 和 next Method


所有迭代器都实现了在 standard 库中定义的名为 Iterator 的 trait。trait 的定义如下所示:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

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

    // methods with default implementations elided
}
}


请注意,此定义使用了一些新语法:type ItemSelf::Item,它们定义了与此 trait 的关联类型。我们将在第 19 章深入讨论关联类型。现在,您需要知道的是,这段代码说实现 Iterator trait 还需要您定义一个 Item 类型,并且这个 Item 类型用于下一个 方法。换句话说,Item 类型将是迭代器返回的类型。


Iterator trait 只需要实现者定义一个方法: next 方法,该方法一次返回一个包裹在 Some 和迭代结束时,返回 None


我们可以直接在迭代器上调用 next 方法;示例 13-12 演示了在从 vector 创建的迭代器上重复调用 next 返回的值。


文件名: src/lib.rs
#[cfg(test)]
mod tests {
    #[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 的调用中获得的值是对 vector 中值的不可变引用。iter 方法在不可变引用上生成迭代器。如果我们想创建一个迭代器来获取 v1 的所有权并返回拥有的值,我们可以调用 into_iter 而不是 iter 的同样,如果我们想迭代可变引用,我们可以调用 iter_mut 而不是 iter


使用 Iterator 的方法


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


调用 next 的方法称为 consuming adapters,因为调用它们会耗尽迭代器。一个例子是 sum 方法,它获取迭代器的所有权,并通过重复调用 next 来迭代项目,从而消耗迭代器。在迭代过程中,它会将每个项目添加到运行总计中,并在迭代完成时返回总计。示例 13-13 有一个测试说明了 sum 方法的用法:


文件名: src/lib.rs
#[cfg(test)]
mod tests {
    #[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 adapters 是在 Iterator trait 上定义的方法,它们不消耗 iterator。相反,它们通过更改原始迭代器的某些方面来生成不同的迭代器。


示例 13-14 显示了一个调用 iterator adapter 方法 map 的示例,该方法在迭代 Item 时需要一个闭包来调用每个 Item。map 方法返回一个新的迭代器,该迭代器生成修改后的项。这里的闭包创建了一个新的迭代器,其中 vector 中的每个项目都将递增 1:


文件名: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

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

示例 13-14:调用迭代器适配器映射以创建新的迭代器


但是,此代码会生成警告:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`


示例 13-14 中的代码什么都没做;我们指定的 closure 永远不会被调用。该警告提醒我们原因:迭代器适配器是惰性的,我们需要在此处使用迭代器。


为了修复这个警告并使用迭代器,我们将使用 collect 方法,我们在第 12 章中使用了该方法,示例 12-1 中的 env::args。此方法使用迭代器并将结果值收集到集合数据类型中。


在示例 13-15 中,我们收集了迭代从 map 调用返回的迭代器的结果,该迭代器被转换为 vector。此 vector 最终将包含原始 vector 中递增 1 的每个项目。


文件名: src/main.rs
fn main() {
    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 方法使用新的迭代器并创建一个 vector


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


您可以将多个调用链接到迭代器适配器,以可读的方式执行复杂的作。但是,由于所有迭代器都是惰性的,因此您必须调用其中一个使用适配器方法,才能从对迭代器适配器的调用中获取结果。


使用捕获其环境的闭包


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


在此示例中,我们将使用采用闭包的 filter 方法。闭包从迭代器中获取一个项目并返回一个 bool。如果闭包返回 true,则该值将包含在由 filter 的 filter 来获取。如果 Closure 返回 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 向量和 shoe size 的所有权作为参数。它返回一个仅包含指定尺码的鞋子的向量。


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


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


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