最近一直在学习 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

要注意两个特征IteratorIntoIterator的区别,一个是实现了迭代器的基本功能,一个是可以转化为迭代器。

有趣的是,每个实现了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语句,但是不能使用breakcontinue,适用于很长的迭代器链。

  • 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 个元素。

…… 迭代器的方法很多,不一而足

迭代器适配器和消费者适配器

顾名思义,迭代器适配器就是产生迭代器,消费者适配器就是消耗迭代器。在之前讲mapfor_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 。