迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。
在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果。
在之前关于流程控制的文章中,介绍For循环的时候,介绍过for循环形式的原理。for循环时间上就是在使用迭代器。不过我们通常使用的形式是简写。
使用方法 | 等价使用方式 | 所有权 |
---|---|---|
for item in collection | for item in IntoIterator::into_iter(collection) | 转移所有权 |
for item in &collection | for item in collection.iter() | 不可变借用 |
for item in &mut collection | for item in collection.iter_mut() | 可变借用 |
for循环能够对迭代器进行循环迭代。(正如上面表格中的等价形式一样,for是对迭代器进行的。)
迭代器之所以成为迭代器,是因为实现了Iterator trait。要实现该特征,最主要的就是实现其中的 next 方法,该方法控制如何从集合中取值,最终返回值的类型是关联类型 Item。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 省略其余有默认实现的方法
}
for 循环通过不停调用迭代器上的 next 方法,来获取迭代器中的元素。我们手动调用next来获取元素。
fn main() {
let num = [1, 2, 3];
let mut iter = num.into_iter();
println!("{}", iter.next().unwrap());
println!("{}", iter.next().unwrap());
println!("{}", iter.next().unwrap());
println!("{}", iter.next().unwrap()); // 迭代器用尽的时候返回值None,因此unwrap会直接panic
}
我们通过into_iter()方法获取了一个迭代器,然后进行迭代。
next 方法对迭代器的遍历是消耗性的,每次消耗它一个元素,最终迭代器中将没有任何元素,只能返回 None。
由于 Vec 动态数组实现了 IntoIterator 特征,因此可以通过 into_iter 将其转换为迭代器,那如果本身就是一个迭代器,该怎么办?实际上,迭代器自身也实现了 IntoIterator,标准库早就帮我们考虑好了:
impl<I: Iterator> IntoIterator for I {
type Item = I::Item;
type IntoIter = I;
#[inline]
fn into_iter(self) -> I {
self
}
}
IntoIterator中实现了into_iter方法,并且该方法返回IntoIterator对象本身。迭代器本身也是可迭代的。类似于python的迭代器不仅实现了__next__
方法,还实现了__iter__
方法,而__iter__
方法就返回了对象本身。因此,我们可以写出下面这样的代码。
for n in num.into_iter().into_iter().into_iter() {
println!("{}", n);
}
这是OK的,因为迭代器本身的into_iter方法返回的就是迭代器本身。同时上面的例子也告诉我们,迭代器的遍历是消耗性的,你传入一个耗尽的迭代器,返回的也是耗尽的迭代器。但是这种链式调用的方式有时候很实用。 在rust里into_ 之类的,都是拿走所有权,_mut 之类的都是可变借用,剩下的就是不可变借用。(大概率,传统习惯上是这样)
rust和python类似,区分了可迭代对象和迭代器对象。Iterator 就是迭代器特征,只有实现了它才能称为迭代器,才能调用 next。 而 IntoIterator 强调的是某一个类型如果实现了该特征,它可以通过 into_iter,iter 等方法变成一个迭代器。称为可迭代对象。
只要迭代器上的某个方法 A 在其内部调用了 next 方法,那么 A 就被称为消费性适配器:因为 next 方法会消耗掉迭代器上的元素,所以方法 A 的调用也会消耗掉迭代器上的元素。其中一个例子是 sum 方法,它会拿走迭代器的所有权,然后通过不断调用 next 方法对里面的元素进行求和:
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
// v1_iter 是借用了 v1,因此 v1 可以照常使用
println!("{:?}",v1);
// 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权
// println!("{:?}",v1_iter);
}
如代码注释中所说明的:在使用 sum 方法后,我们将无法再使用 v1_iter,因为 sum 拿走了该迭代器的所有权。sum的源码如下所示:
fn sum<S>(self) -> S
where
Self: Sized,
S: Sum<Self::Item>,
{
Sum::sum(self)
}
既然消费者适配器是消费掉迭代器,然后返回一个值。那么迭代器适配器,顾名思义,会返回一个新的迭代器,这是实现链式方法调用的关键。与消费者适配器不同,迭代器适配器是惰性的,意味着你需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:
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]);
上面代码中,使用了 collect 方法,该方法就是一个消费者适配器,使用它可以将一个迭代器中的元素收集到指定类型中,这里我们为 v2 标注了 Vec<> 类型,就是为了告诉 collect:请把迭代器中的元素消费掉,然后把值收集成 Vec<> 类型,至于为何使用 _,因为编译器会帮我们自动推导。
map 会对迭代器中的每一个值进行一系列操作,然后把该值转换成另外一个新值,该操作是通过闭包 |x| x + 1 来完成:最终迭代器中的每个值都增加了 1,从 [1, 2, 3] 变为 [2, 3, 4]。
zip 把两个迭代器合并成一个迭代器,新迭代器中,每个元素都是一个元组,由之前两个迭代器的元素组成。例如将形如 [1, 2, 3, 4, 5] 和 [2, 3, 4, 5] 的迭代器合并后,新的迭代器形如 [(1, 2),(2, 3),(3, 4),(4, 5)]
filter 对迭代器中的元素进行过滤,例如将形如 [1, 2, 3, 4, 5]的数组经过filter传递的闭包|x| x % 2 == 0处理,则保留元素[2, 4]
创建一个计数器:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
我们为计数器 Counter 实现了一个关联函数 new,用于创建新的计数器实例。下面我们继续为计数器实现 Iterator 特征:
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
最后,使用迭代器。
fn main() {
let count = Counter::new();
for c in count {
println!("{}", c);
}
}
可以看出,实现自己的迭代器非常简单,但是 Iterator 特征中,不仅仅是只有 next 一个方法,那为什么我们只需要实现它呢?因为其它方法都具有默认实现,所以无需像 next 这样手动去实现,而且这些默认实现的方法其实都是基于 next 方法实现的。
下面的代码演示了部分方法的使用:
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
assert_eq!(18, sum);