使用迭代器处理一系列条目
迭代器模式允许你依次对一系列条目执行某些任务。迭代器负责迭代每个条目的逻辑,并确定序列何时完成。当你使用迭代器时,你无需自己重新实现该逻辑。
在 Rust 中,迭代器是惰性的,这意味着除非你调用消耗迭代器的方法来使用它,否则它们不会产生任何效果。例如,列表 13-10 中的代码通过调用在 Vec<T>
上定义的 iter
方法,在向量 v1
中的条目上创建了一个迭代器。这段代码本身没有任何实际作用。
迭代器存储在 v1_iter
变量中。一旦我们创建了一个迭代器,我们就可以以各种方式使用它。在第 3 章的列表 3-5 中,我们使用 for
循环迭代了一个数组,以便对其每个条目执行一些代码。在底层,这隐式地创建然后消耗了一个迭代器,但我们一直忽略了这到底是如何工作的,直到现在。
在列表 13-11 的示例中,我们将迭代器的创建与 for
循环中迭代器的使用分离开来。当使用 v1_iter
中的迭代器调用 for
循环时,迭代器中的每个元素都在循环的一次迭代中使用,从而打印出每个值。
for
循环中使用迭代器在标准库未提供迭代器的语言中,你可能会通过将变量从索引 0 开始,使用该变量索引到向量以获取值,并在循环中递增变量值直到达到向量中的条目总数来编写相同的功能。
迭代器为你处理所有这些逻辑,减少了你可能搞砸的重复代码。迭代器为你提供了更大的灵活性,可以将相同的逻辑用于许多不同类型的序列,而不仅仅是你可以索引到的数据结构,例如向量。让我们检查一下迭代器是如何做到这一点的。
Iterator
trait 和 next
方法
所有迭代器都实现了一个名为 Iterator
的 trait,该 trait 在标准库中定义。trait 的定义如下所示
请注意,此定义使用了一些新语法:type Item
和 Self::Item
,它们正在为此 trait 定义一个关联类型。我们将在第 20 章深入讨论关联类型。现在,你只需要知道这段代码表示实现 Iterator
trait 也需要你定义一个 Item
类型,并且此 Item
类型在 next
方法的返回类型中使用。换句话说,Item
类型将是从迭代器返回的类型。
Iterator
trait 仅要求实现者定义一个方法:next
方法,该方法一次返回迭代器的一个条目,并将其包装在 Some
中,当迭代结束时,返回 None
。
我们可以直接在迭代器上调用 next
方法;列表 13-12 演示了从对从向量创建的迭代器重复调用 next
返回的值。
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
方法的用法
sum
方法以获取迭代器中所有条目的总和在调用 sum
之后,我们不允许使用 v1_iter
,因为 sum
取得了我们调用它的迭代器的所有权。
生成其他迭代器的方法
迭代器适配器是在 Iterator
trait 上定义的方法,它们不会消耗迭代器。相反,它们通过更改原始迭代器的某些方面来生成不同的迭代器。
列表 13-14 显示了调用迭代器适配器方法 map
的示例,该方法接受一个闭包,以便在迭代条目时在每个条目上调用。map
方法返回一个新的迭代器,该迭代器生成修改后的条目。此处的闭包创建一个新的迭代器,其中向量中的每个条目将递增 1
map
以创建新的迭代器但是,此代码会产生警告
$ 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 中的代码没有任何作用;我们指定的闭包永远不会被调用。警告提醒我们原因:迭代器适配器是惰性的,我们需要在此处消耗迭代器。
为了修复此警告并消耗迭代器,我们将使用 collect
方法,我们在第 12 章中与列表 12-1 中的 env::args
一起使用了该方法。此方法消耗迭代器并将结果值收集到集合数据类型中。
在列表 13-15 中,我们将迭代从调用 map
返回的迭代器的结果收集到一个向量中。此向量最终将包含原始向量中每个条目递增 1 的值。
map
方法以创建新的迭代器,然后调用 collect
方法以消耗新的迭代器并创建向量由于 map
接受闭包,我们可以指定要对每个条目执行的任何操作。这是一个很好的例子,说明了闭包如何在重用 Iterator
trait 提供的迭代行为的同时,让你自定义某些行为。
你可以链式调用多个迭代器适配器来以可读的方式执行复杂的操作。但是由于所有迭代器都是惰性的,因此你必须调用消耗型适配器方法之一才能从迭代器适配器的调用中获得结果。
使用捕获其环境的闭包
许多迭代器适配器将闭包作为参数,通常,我们将指定为迭代器适配器参数的闭包将是捕获其环境的闭包。
对于此示例,我们将使用 filter
方法,该方法接受一个闭包。闭包从迭代器获取一个条目并返回一个 bool
。如果闭包返回 true
,则该值将包含在 filter
生成的迭代中。如果闭包返回 false
,则该值将不包含在内。
在列表 13-16 中,我们将 filter
与捕获其环境中的 shoe_size
变量的闭包一起使用,以迭代 Shoe
结构实例的集合。它将仅返回指定尺寸的鞋子。
#[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")
},
]
);
}
}
filter
方法与捕获 shoe_size
的闭包一起使用shoes_in_size
函数取得鞋子的向量和鞋子尺寸作为参数的所有权。它返回一个仅包含指定尺寸鞋子的向量。
在 shoes_in_size
的主体中,我们调用 into_iter
以创建一个取得向量所有权的迭代器。然后,我们调用 filter
以将该迭代器适配为一个新的迭代器,该迭代器仅包含闭包返回 true
的元素。
闭包从环境中捕获 shoe_size
参数,并将该值与每只鞋子的尺寸进行比较,仅保留指定尺寸的鞋子。最后,调用 collect
将适配后的迭代器返回的值收集到一个向量中,该向量由函数返回。
测试表明,当我们调用 shoes_in_size
时,我们只会获得与我们指定的值尺寸相同的鞋子。