Rust 迭代器 Iterator & IntoIterator

1,405 阅读10分钟

迭代器允许我们迭代一个连续的集合,例如数组、动态数组 Vec、HashMap 等,在此过程中,只需关心集合中的元素如何处理,而无需关心如何开始、如何结束、按照什么样的索引去访问等问题。

For 循环与迭代器

迭代器与 for 循环最主要的差别就在于:是否通过索引来访问集合。严格来说,Rust 中的 for 循环是编译器提供的语法糖,最终还是对迭代器中的元素进行遍历。在 Rust 中,实现了 IntoIterator trait 的类型都可以自动把类型集合转换为迭代器,然后通过 for 语法糖进行访问。

例如数组:

let arr = [1, 2, 3];
for v in arr {
    peintln!("{}", v);
}

也是可以使用 IntoIterator trait 的 into_iter 方法显式地将数组转换成迭代器。

let arr = [1, 2, 3];
for v in arr.into_iter() { 
   println!("{}", v); 
}

注:数组不是迭代器(没有实现Iterator),但是数组实现了IntoIterator,Rust 通过 for 语法糖,自动把实现了该特征的数组类型转换为迭代器,最终让我们可以直接对一个数组进行迭代。

此外,还可以使用 for 循环对数值序列进行迭代,如 

for i in 1..100 { ... };// 有限迭代器:对有限数值序列进行迭代

for i in (1..).into_iter() {...}; // 无限迭代器:无限长度的自增序列

Iterator trait 迭代器

Iterator trait 定义:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 省略其余有默认实现的方法
}

在 Rust 中,迭代器是惰性的,也就是在定义迭代器后不使用将不会发生任何事,只有在使用时迭代器才会开始迭代其中的元素。

惰性初始化的方式确保了创建迭代器不会有任何额外的性能损耗,其中的元素也不会被消耗,只有使用到该迭代器的时候,一切才开始。

迭代器之所以成为迭代器,就是因为实现了 Iterator trait,要实现该 trait,最主要的就是要实现其中的 next 方法。for 循环通过不断调用迭代器上的 next 方法来获取迭代器中的元素。

比如:

let arr = [1, 2, 3]; 
let mut arr_iter = arr.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);

迭代器本身也可以直接调用 next 方法,返回值是 Option 类型(有值时是Some(T),无值时是None),遍历是按照迭代器中元素的排列顺序依次进行的,同时手动迭代必须将迭代器声明为 mut 可变,因为调用 next 会改变迭代器其中的状态数据,使用 for 循环时则无需标注,因为for循环自动完成。

for 循环是迭代器的语法糖,大概原理如下:


let values = vec![1, 2, 3];

{
    /// IntoIterator::into_iter完全限定的方式与into_iter方式values.into_iter()是等价的
    let result = match IntoIterator::into_iter(values) {
        /// 使用了loop循环配合next方法来遍历迭代器中的元素,当迭代器返回 None时,跳出循环
        mut iter => loop {
            match iter.next() {
                Some(x) => { println!("{}", x); },
                None => break,
            }
        },
    };
    result
}

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

IntoIterator trait 转化成迭代器

IntoIterator trait 定义:

trait IntoIterator 
where
    <Self::IntoIter as Iterator>::Item == Self::Item, 
{
    type Item;
    type IntoIter: Iterator;
    fn into_iter(self) -> Self::IntoIter;
}

实现 IntoIterator 特性的类型可以被转换为迭代器。当用于 for-in 循环时,将自动调用该类型的 into_iter 方法。

比如动态 Vec 不仅实现了IntoIteratortrait,&Vec 与 &mut Vec 同样如此。因此我们可以相应的对可变与不可变的引用,以及自有值进行迭代。

Vec 实现了三种不同形式的 IntoInterator:

impl<T> IntoIterator for Vec<T>
impl<'a, T> IntoIterator for &'a Vec<T>
impl<'a, T> IntoIterator for &'a mut Vec<T>

根据被遍历对象的类型,for 循环会选择使用哪个into_iter方法并获得不同类型的元素:

  1. 可遍历对象的共享引用返回共享引用的元素 &T
  2. 可遍历对象的可变引用返回可变引用的元素 &mut T
  3. 拥有所有权的可遍历对象返回值 T。注意:这个可遍历对象被转移所有权,从而被会 drop 掉。
// vec = Vec<T>
for v in &vec {} // v = &T

// above example desugared
// 以上代码等价于
for v in (&vec).into_iter() {}

// vec = Vec<T>
for v in &mut vec {} // v = &mut T

// above example desugared
// 以上代码等价于
for v in (&mut vec).into_iter() {}

标准库的一揽子实现机制为迭代器Iterator本身自动实现了 IntoIterator trait:

impl<I: Iterator> IntoIterator for I { 
  type Item = I::Item; 
  type IntoIter = I; 
  
  #[inline] 
  fn into_iter(self) -> I {
    self 
  }
}

所以 vec.into_iter() 与 vec.into_iter().into_iter().into_iter() 效果相同的。

fn main() { 
  let values = vec![1, 2, 3]; 
  for v in values.into_iter().into_iter().into_iter() { 
    println!("{}",v) 
  } 
}

into_iter,iter,iter_mut 产生迭代器的区别

  • into_iter() 返回 T,&T 或 &mut T 类型的 Iterator, 依赖于环境;
  • iter() 迭代引用(&T):调用 next 方法返回的类型是 Some(&T)
  • iter_mut() 迭代可变引用(&mut T):调用 next 方法返回的类型是 Some(&mut T)

注:Rust命名规律:into_ 之类的,都是拿走所有权,_mut 之类的都是可变引用,剩下的就是不可变引用。

代码示例:

fn main() {
    let values = vec![1, 2, 3];

    // 所有权转移至迭代器中,values 将不能再使用
    for v in values.into_iter() {
        println!("{}", v)
    }

    // 下面的代码将报错,因为 values 的所有权在上面 `for` 循环中已经被转移走
    // println!("{:?}",values);

    let values = vec![1, 2, 3];
    let _values_iter = values.iter();

    // 不会报错,因为 values_iter 只是借用了 values 中的元素
    println!("{:?}", values);

    let mut values = vec![1, 2, 3];
    // 对 values 中的元素进行可变借用
    let mut values_iter_mut = values.iter_mut();

    // 取出第一个元素,并修改为0
    if let Some(v) = values_iter_mut.next() {
        *v = 0;
    }

    // 输出[0, 2, 3]
    println!("{:?}", values);
}

Iterator 和 IntoIterator 的区别

Iterator 是迭代器 trait,只有实现了它才能称为迭代器,才能调用 next 方法。

IntoIterator 强调的是某一个类型如果实现了该 trait,那么该类型数据可以通过 into_iter() 、iter() 或 iter_mut() 方法将其变成一个迭代器。

实现 Iterator<Item = T> 的类型可以迭代产生 T 类型。注意:并不存在 IteratorMut 类型,因为可以通过在实现 Iterator 特性时指定 Item 关联类型,来选择其返回的是不可变引用、可变引用还是自有值。

Vec<T> 方法返回类型
.iter()Iterator<Item = &T>
.iter_mut()Iterator<Item = &mut T>
.into_iter()Iterator<Item = T>

消费者与适配器

消费者是迭代器上的方法,它会消费掉迭代器中的元素,然后返回该类型的值,这些消费者都有一个共同的特点:在定义中都依赖 next 方法来消费元素。

消费性适配器:某些方法(比如collect、 sum等)在其内部会自动调用 next方法,并会拿走迭代器的所有权,消耗掉迭代器上的元素。比如 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);
}

迭代器适配器:Iterator trait 中定义了另一类方法,允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。

let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);  // 警告,因为map方法返回一个迭代器,map 迭代器是惰性的,不产生任何效果
let v2: Vec<> = v1.iter().map(|x| x + 1).collect();  // 将v1中的数依次加1然后生成一个新的Vec,其中collect()方法就是一个消费者适配器

collect 方法是一个消费者适配器,可以将一个迭代器中的元素收集到指定类型中(注意:必须显式的告诉编译器想要收集成的集合类型),上面代码示例中的 Vec<_> 表明将迭代器中的元素收集成 Vec 类型,具体元素类型通过类型推导获得。

map 方法是一个迭代器适配器,会将迭代器中的每一个值进行一系列操作,然后把该值转换成另外一个新值,该操作通过闭包完成。

let names = ["tim", "tony"];
let ages = [11, 33];
let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();

zip 是一个迭代器适配器,其作用就是将两个迭代器的内容压缩到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)> 这样的新的迭代器,然后通过 collect 方法将迭代器中的(K, V)形式的值收集成 HashMap<K, V>

之前的 map 方法中使用闭包作为迭代器适配器的参数,其最大的好处不仅在于可以就地实现迭代器中元素的处理,还在于可以捕获环境值。下面的代码示例同时体现了这两个优点:

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()  //捕获外部环境中的变量 shoe_size 来与输入参数进行比较
}

filter 是迭代器适配器,用于对迭代器中的每个值进行过滤。它使用闭包作为参数,该闭包从迭代器中获取一项并返回一个 bool,如果闭包返回 true,其值将会包含在 filter 提供的新迭代器中。

为自定义类型实现 Iterator trait

前面使用了数组来创建迭代器,但其实基于其它集合类型一样可以创建迭代器,也可以创建自己的迭代器——只要为自定义类型实现 Iterator trait 即可。中文标准库中的 Module std::iter 中给出了如何实现迭代器的方法。

例如:

struct MyType {
    items: Vec<String>
}

impl MyType {
    fn iter(&self) -> impl Iterator<Item = &String> {
        MyTypeIterator {
            index: 0,
            items: &self.items
        }
    }
}

struct MyTypeIterator<'a> {
    index: usize,
    items: &'a Vec<String>
}

impl<'a> Iterator for MyTypeIterator<'a> {
    type Item = &'a String;
    fn next(&mut self) -> Option<Self::Item> {
        if self.index >= self.items.len() {
            None
        } else {
            let item = &self.items[self.index];
            self.index += 1;
            Some(item)
        }
    }
}

实现自己的迭代器非常简单,但是 Iterator trait 中,只需要自己实现 next 一个方法,因为其他方法都有默认实现,并且这些默认实现的方法其实都是基于 next 方法实现的。

比如enumerate 是 Iterator trait 上的方法,该方法产生一个新的迭代器,其每个元素都是元素(索引,值)。enumerate 是迭代器适配器,可以使用消费者迭代器或 for 循环对新产生的迭代器进行处理。

代码示例如下:

let v = vec![1u64, 2, 3, 4, 5, 6];
for (i, v) in v.iter().enumerate() {
    println!("第{}个值是{}", i, v)
}

迭代器的性能

根据测试,使用迭代器和 for 循环完成同样的任务,迭代器的运行时间还要少一点。

迭代器是 Rust 的零成本抽象之一,这就意味着抽象并不会引入运行时开销。

总之,迭代器是 Rust 受函数式语言启发而提供的高级语言特性,可以写出更加简洁、逻辑清晰的代码。编译器还可以通过循环展开(Unrolling)、向量化、消除边界检查等优化手段,使得迭代器和 for 循环都有极为高效的执行效率。

总结

Rust 中实现了 Iterator trait 才是迭代器,实现 IntoIterator 特性的类型可以被转换为迭代器,提供三个公共方法创建迭代器:iter()、iter_mut() 和 into_iter(),分别用于迭代 &T(引用)、&mut T(可变引用)和 T(值),其中,前两种是普通方法,而into_iter() 来自于 IntoIterator trait。

Rust 的 for 循环其实是迭代器语法糖,当没有显式的使用迭代器时,它会根据不同的上下文,分别使用 T、&T 和 &mut T 类型所实现的 into_iter() 返回的迭代器。

遍历类型返回不同遍历器的三个方法:

  • into_iter() 返回 T,&T 或 &mut T 类型的 Iterator,依赖于环境,IntoIterator的方法;
  • iter()返回 Iterator<&T>,调用 next 方法返回的类型是 Some(&T)
  • iter_mut()返回 Iterator<&mut T>, 调用 next 方法返回的类型是 Some(&mut T)

参考