最近一直在学习 Rust 语言,深感它的学习曲线很陡峭,因此会写几篇博客记录一下学习进度。
本文主要参考了 Rust Course 和 Rust 标准库文档。
什么是迭代器
迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,设计人员使用此接口无需关心容器对象的内存分配的实现细节。
在 Rust 中的例子:
- 使用
for
的隐式迭代器
let v = vec![1, 2, 3];
for i in &v {
println!("{}", i);
}
for
语句中显式使用迭代器
let v = vec![1, 2, 3];
for i in v.into_iter() {
println!("{}", i);
}
- 使用
Iterator
特征的next
方法
let v = vec![1, 2, 3];
let mut v = v.into_iter();
assert_eq!(arr_iter.next(), Some(1));
assert_eq!(arr_iter.next(), Some(2));
assert_eq!(arr_iter.next(), Some(3));
assert_eq!(arr_iter.next(), None);
当使用for a in b
语句遍历时,如果对象 b 实现了IntoIterator
特性,我们便可以利用 Rust 给我们提供的语法糖,不必显式使用迭代器。
迭代器是消耗性的,每遍历到一个元素就会消耗掉一个元素,最终迭代器中没有任何元素,只能返回None
。
要注意两个特征
Iterator
和IntoIterator
的区别,一个是实现了迭代器的基本功能,一个是可以转化为迭代器。
有趣的是,每个实现了
Iterator
的类型都实现了IntoIterator
,调用into_iter
时会返回自身。
对Iterator
更进一步
Iterator
的路径为std::iter::Iterator
,拥有一个关联类型Item
,和迭代的元素类型相同。
该特性有 72 个方法,下面着重介绍几种常用方法:
next
方法
fn next(&mut self) -> Option<Self::Item>;
返回Some(Item)
或者None
,同时更新自己到下一个位置。
size_hint
方法
fn size_hint(&self) -> (usize, Option<usize>);
返回一个元组(lower, upper)
,lower
为当前迭代器剩余元素个数的下界, upper
为其上界。
如果上界很大,超过了
usize
的最大值,就会是None
,因此size_hint
的默认实现返回的就是(0, None)
,这满足任意一个迭代器。
这个方法的主要作用是优化代码性能,可以通过该方法为迭代器的元素保留空间。在
std
的官方文档中提到,该实现应当提供正确的估计,不能因为实现不正确导致内存安全性原则。
enumerate
方法
fn enumerate(self) -> Enumerate<Self>;
返回一个迭代器,迭代器的元素为一个元组(i, val)
,i
为当前元素的索引,val
为当前元素。
map
方法
fn map<B, F>(self, f: F) -> Map<Self, F>ⓘ
where
F: FnMut(Self::Item) -> B,
{ ... }
F
是一个闭包,通过对每个元素调用该闭包,map
将一个迭代器转换为另一个迭代器。
值得注意的是,map
是一个非常“函数式”的方法,如果使用map
方法调用一些有副作用的闭包,由于懒惰执行,它可能根本不会起作用:
(0..5).map(|x| println!("{}", x));
for_each
方法
fn map<B, F>(self, f: F)
where
F: FnMut(Self::Item)
效果上类似与for
语句,但是不能使用break
和continue
,适用于很长的迭代器链。
filter
方法
fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
P: FnMut(&Self::Item) -> bool
创建一个迭代器,该迭代器使用闭包确定是否应产生元素。
filter_map
方法
fn filter_map<B, F>(self, f: F) -> FilterMap<Self, F>
where
F: FnMut(Self::Item) -> Option<B>
既过滤又映射,可以用于简化迭代器链。
zip
方法
fn zip<U>(self, other: U) -> Zip<Self, <U as IntoIterator>::IntoIter>
where
U: IntoIterator
将两个迭代器压缩为成对的单个迭代器。
如果任一迭代器返回
None
,则zipped
迭代器中的next
将返回None
。如果第一个迭代器返回None
,则zip
将短路,并且不会在第二个迭代器上调用next
。
fold
方法
fn fold<B, F>(self, init: B, f: F) -> B
where
F: FnMut(B, Self::Item) -> B
F
被称为累加器,通过应用操作将每个元素 fold 到一个累加器中,返回最终结果。
看一个例子:
let a = [1, 2, 3];
let sum = a.iter().fold(0, |acc, x| acc + x);
assert_eq!(sum, 6);
collect
方法
fn collect<B>(self) -> B
where
B: FromIterator<Self::Item>
将迭代器转换为集合。
collect
需要上下文让其知道按照什么方式收集,一种是通过直接显式地表明类型,一种是显式地指定collect
调用时的类型。
let v: vec<_> = (1..5).collect();
// or
v = (1..5).collect::<vec<_>>();
skip
方法和take
方法
take(n)
的作用是取前 n 个元素,而skip(n)
正好相反,跳过前 n 个元素。
…… 迭代器的方法很多,不一而足
迭代器适配器和消费者适配器
顾名思义,迭代器适配器就是产生迭代器,消费者适配器就是消耗迭代器。在之前讲map
和for_each
时可以看到,map
创建了一个迭代器,而且是懒惰的,在使用之前不产生任何行为,如果map
在调用链的最后一个编译器就会报警,因此我们需要一个消费者适配器来收尾。
warning: unused `Map` that must be used
--> c.rs:2:5
|
2 | (0..5).map(|x| println!("{}", x));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: iterators are lazy and do nothing unless consumed
正确的例子:
let v: Vec<_> = (1..5).map(|x| x * 2).collect();
自己动手实现Iterator
特征
主要是要实现Iterator
中的next
方法,这个方法是其余方法的基础,实现了next
方法后,其余都可以默认实现。
struct Range {
cnt: i32,
end: i32
}
impl Range {
fn new(cnt: i32, end: i32) -> Range {
Range {
cnt,
end
}
}
}
impl Iterator for Range {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
self.cnt += 1;
if self.cnt == self.end {
None
} else {
Some(self.cnt)
}
}
}
fn main() {
let mut r = Range::new(0, 10);
assert_eq!(r.next(), Some(1));
assert_eq!(r.next(), Some(2));
assert_eq!(r.next(), Some(3));
assert_eq!(r.next(), Some(4));
let sum: i32 = Range::new(0, 10).sum();
assert_eq!(sum, 45);
let sum: i32 = Range::new(0, 10).map(|x| x * 2).filter(|x| x < &10).sum();
assert_eq!(sum, 20);
}
总结
迭代器是 Rust 的零成本抽象之一,抽象时不会引起运行时开销,所以迭代器有着极高的运行效率。
在编写代码时,使用迭代器也能带来良好的函数式编程的体验,使用迭代器链并与闭包结合时体验更是超过了 Lisp 。