rust迭代器

2023-05-26 10:41:26 浏览数 (2)

迭代器(Iterator)

迭代器模式允许你对一个序列的项进行某些处理。迭代器(iterator)负责遍历序列中的每一项和决定序列何时结束的逻辑。当使用迭代器时,我们无需重新实现这些逻辑。

在 Rust 中,迭代器是 惰性的(lazy),这意味着在调用方法使用迭代器之前它都不会有效果

For循环和迭代器

在之前关于流程控制的文章中,介绍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是对迭代器进行的。)

next方法

迭代器之所以成为迭代器,是因为实现了Iterator trait。要实现该特征,最主要的就是实现其中的 next 方法,该方法控制如何从集合中取值,最终返回值的类型是关联类型 Item。

代码语言:javascript复制
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 省略其余有默认实现的方法
}

for 循环通过不停调用迭代器上的 next 方法,来获取迭代器中的元素。我们手动调用next来获取元素。

代码语言:javascript复制
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()方法获取了一个迭代器,然后进行迭代。

  1. next 方法返回的是 Option 类型,当有值时返回 Some(i32),无值时返回 None
  2. 遍历是按照迭代器中元素的排列顺序依次进行的,因此我们严格按照数组中元素的顺序取出了 Some(1),Some(2),Some(3)
  3. 手动迭代必须将迭代器声明为 mut 可变,因为调用 next 会改变迭代器其中的状态数据(当前遍历的位置等),而 for 循环去迭代则无需标注 mut,因为它会帮我们自动完成

next 方法对迭代器的遍历是消耗性的,每次消耗它一个元素,最终迭代器中将没有任何元素,只能返回 None。

IntoIterator 特征

由于 Vec 动态数组实现了 IntoIterator 特征,因此可以通过 into_iter 将其转换为迭代器,那如果本身就是一个迭代器,该怎么办?实际上,迭代器自身也实现了 IntoIterator,标准库早就帮我们考虑好了:

代码语言:javascript复制
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__方法就返回了对象本身。因此,我们可以写出下面这样的代码。

代码语言:javascript复制
for n in num.into_iter().into_iter().into_iter() {
    println!("{}", n);
}

这是OK的,因为迭代器本身的into_iter方法返回的就是迭代器本身。同时上面的例子也告诉我们,迭代器的遍历是消耗性的,你传入一个耗尽的迭代器,返回的也是耗尽的迭代器。但是这种链式调用的方式有时候很实用。 在rust里into_ 之类的,都是拿走所有权,_mut 之类的都是可变借用,剩下的就是不可变借用。(大概率,传统习惯上是这样)

IntoIterator和Iterator

rust和python类似,区分了可迭代对象和迭代器对象。Iterator 就是迭代器特征,只有实现了它才能称为迭代器,才能调用 next。 而 IntoIterator 强调的是某一个类型如果实现了该特征,它可以通过 into_iter,iter 等方法变成一个迭代器。称为可迭代对象

消费者适配器

只要迭代器上的某个方法 A 在其内部调用了 next 方法,那么 A 就被称为消费性适配器:因为 next 方法会消耗掉迭代器上的元素,所以方法 A 的调用也会消耗掉迭代器上的元素。其中一个例子是 sum 方法,它会拿走迭代器的所有权,然后通过不断调用 next 方法对里面的元素进行求和:

代码语言:javascript复制
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的源码如下所示:

代码语言:javascript复制
fn sum<S>(self) -> S
where
    Self: Sized,
    S: Sum<Self::Item>,
{
    Sum::sum(self)
}

迭代器适配器

既然消费者适配器是消费掉迭代器,然后返回一个值。那么迭代器适配器,顾名思义,会返回一个新的迭代器,这是实现链式方法调用的关键。与消费者适配器不同,迭代器适配器是惰性的,意味着你需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:

代码语言:javascript复制
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

上面代码中,使用了 collect 方法,该方法就是一个消费者适配器,使用它可以将一个迭代器中的元素收集到指定类型中,这里我们为 v2 标注了 Vec<> 类型,就是为了告诉 collect:请把迭代器中的元素消费掉,然后把值收集成 Vec<> 类型,至于为何使用 _,因为编译器会帮我们自动推导。

map

map 会对迭代器中的每一个值进行一系列操作,然后把该值转换成另外一个新值,该操作是通过闭包 |x| x 1 来完成:最终迭代器中的每个值都增加了 1,从 [1, 2, 3] 变为 [2, 3, 4]。

zip

zip 把两个迭代器合并成一个迭代器,新迭代器中,每个元素都是一个元组,由之前两个迭代器的元素组成。例如将形如 [1, 2, 3, 4, 5] 和 [2, 3, 4, 5] 的迭代器合并后,新的迭代器形如 [(1, 2),(2, 3),(3, 4),(4, 5)]

filter

filter 对迭代器中的元素进行过滤,例如将形如 [1, 2, 3, 4, 5]的数组经过filter传递的闭包|x| x % 2 == 0处理,则保留元素[2, 4]

实现 Iterator 特征

创建一个计数器:

代码语言:javascript复制
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

我们为计数器 Counter 实现了一个关联函数 new,用于创建新的计数器实例。下面我们继续为计数器实现 Iterator 特征:

代码语言:javascript复制
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
        }
    }
}

最后,使用迭代器。

代码语言:javascript复制
fn main() {
    let count = Counter::new();

    for c in count {
        println!("{}", c);
    }
}

可以看出,实现自己的迭代器非常简单,但是 Iterator 特征中,不仅仅是只有 next 一个方法,那为什么我们只需要实现它呢?因为其它方法都具有默认实现,所以无需像 next 这样手动去实现,而且这些默认实现的方法其实都是基于 next 方法实现的。

下面的代码演示了部分方法的使用:

代码语言:javascript复制
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);

参考资料

rust程序设计语言

0 人点赞