使用迭代器处理一系列项目
迭代器模式允许您依次对一系列项目执行某些任务。迭代器负责迭代每个项并确定序列何时完成的逻辑。当您使用迭代器时,您不必自己重新实现该逻辑。
在 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 Item
和 Self::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
时,我们只返回与我们指定的值具有相同尺码的鞋子。