5.5.迭代器

优质
小牛编辑
129浏览
2023-12-01

让我们讨论一下循环。

还记得 Rust 的for循环吗?这是一个例子:

for x in 0..10 {
    println!("{}", x);
}

现在我们更加了解 Rust 了,我们可以谈谈这里的具体细节了。这个范围(0..10)是“迭代器”。我们可以重复调用迭代器的.next()方法,然后它会给我们一个数据序列。

(另外,像0..10带有两个点号的 range 是包含左边(从 0 开始)但不包含右边的值(到 9 为止)。一个数学家会这么写“[0, 10)”。为了得到一个一个一直到 10 的 range 你可以写成0...10。)

就像这样:

let mut range = 0..10;

loop {
    match range.next() {
        Some(x) => {
            println!("{}", x);
        },
        None => { break }
    }
}

我们创建了一个range的可变绑定,它是我们的迭代器。我们接着loop,它包含一个matchmatch用来匹配range.next()的结果,它给我们迭代器的下一个值。next返回一个Option<i32>,在这个例子中,如果有值,它会返回Some(i32)然后当我们循环完毕,就会返回None。如果我们得到Some(i32),我们就会打印它,如果我们得到None,我们break出循环。

这个代码例子基本上和我们的loop版本一样。for只是loop/match/break结构的简便写法。

然而,for循环并不是唯一使用迭代器的结构。编写你自己的迭代器涉及到实现Iterator特性。然而特性不是本章教程的涉及范围,不过 Rust 提供了一系列的有用的迭代器帮助我们完成各种任务。但首先注意下范围 的一些局限性。

范围 非常原始,我们通常可以用更好的替代方案。考虑下面的 Rust 反模式:用范围 来模拟 C-风格的for循环。比如你想遍历完 vector 的内容。你可能尝试这么写:

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

for i in 0..nums.len() {
    println!("{}", nums[i]);
}

这严格的说比使用现成的迭代器还要糟。你可以直接在 vector 上遍历。所以这么写:

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

for num in &nums {
    println!("{}", num);
}

这么写有两个原因。第一,它更明确的表明了我们的意图。我们迭代整个向量,而不是先迭代向量的索引,再按索引取值。第二,这个版本也更有效率:第一个版本会进行额外的边界检查因为它使用了索引,nums[i]。因为我们利用迭代器获取每个向量元素的引用,第二个例子中并没有边界检查。这在迭代器中非常常见:我们可以忽略不必要的边界检查,不过仍然知道我们是安全的。

这里还有一个细节不是 100% 清楚的就是println!是如何工作的。num&i32类型。也就是说,它是一个i32的引用,并不是i32本身。println!为我们处理了解引用,所以我们并没有看到它。下面的代码也能工作:

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

for num in &nums {
    println!("{}", *num);
}

现在我们显式的解引用了num。为什么&nums会给我们一个引用呢?首先,因为我们显式的使用了&。其二,如果它给我们数据,我们就是它的所有者了,这会涉及到生成数据的拷贝然后返回给我们拷贝。通过引用,我们只是借用了一个数据的引用,所以仅仅是传递了一个引用,并不涉及数据的移动。

那么,既然现在我们已经明确了范围通常不是我们需要的,那么让我们来讨论下你真正需要什么。

这里涉及到大体上相关的3类事物:迭代器,迭代适配器iterator adapters)和消费者consumers)。下面是一些定义:

  • 迭代器 给你一个值的序列
  • 迭代适配器 操作迭代器,产生一个不同输出序列的新迭代器
  • 消费者 操作迭代器,产生最终值的集合

让我们先看看消费者,因为我们已经见过范围这个迭代器了。

消费者(Consumers)

消费者 操作一个迭代器,返回一些值或者几种类型的值。最常见的消费者是collect()。这个代码还不能编译,不过它表明了我们的意图:

let one_to_one_hundred = (1..101).collect();

如你所见,我们在迭代器上调用了collect()collect()从迭代器中取得尽可能多的值,然后返回结果的集合。那么为什么这不能编译呢?因为Rust不能确定你想收集什么类型的值,所以你需要让它知道。下面是一个可以编译的版本:

let one_to_one_hundred = (1..101).collect::<Vec<i32>>();

如果你还记得,::<>语法允许我们给出一个类型提示,所以我们可以告诉编译器我们需要一个整型的向量。但是你并不总是需要提供完整的类型。使用_可以让你提供一个部分的提示:

let one_to_one_hundred = (1..101).collect::<Vec<_>>();

这是指“请把值收集到Vec<T>,不过自行推断T类型”。为此_有时被称为“类型占位符”。

collect()是最常见的消费者,不过这还有其它的消费者。find()就是一个:

let greater_than_forty_two = (0..100)
                             .find(|x| *x > 42);

match greater_than_forty_two {
    Some(_) => println!("Found a match!"),
    None => println!("No match found :("),
}

find接收一个闭包,然后处理迭代器中每个元素的引用。如果这个元素是我们要找的,那么这个闭包返回true,如果不是就返回false。因为我们可能不能找到任何元素,所以find返回Option而不是元素本身。

另一个重要的消费者是fold。他看起来像这样:

let sum = (1..4).fold(0, |sum, x| sum + x);

fold()看起来像这样:fold(base, |accumulator, element| ...)。它需要两个参数:第一个参数叫做基数base)。第二个是一个闭包,它自己也需要两个参数:第一个叫做累计数accumulator),第二个叫元素element)。每次迭代,这个闭包都会被调用,返回值是下一次迭代的累计数。在我们的第一次迭代,累计数的值是基数。

好吧,这有点混乱。让我们检查一下这个迭代器中所有这些值:

基数累计数元素闭包结果
0011
0123
0336

我们可以使用这些参数调用fold()

# (1..4)
.fold(0, |sum, x| sum + x);

那么,0是我们的基数,sum是累计数,x是元素。在第一次迭代,我们设置sum0,然后xnums的第一个元素,1。我们接着把sumx相加,得到0 + 1 = 1。在我们第二次迭代,sum成为我们的累计值,元素是数组的第二个值,21 + 2 = 3,然后它就是最后一次迭代的累计数。在这次迭代中,x是最后的元素,3,那么3 + 3 = 6,就是我们和的最终值。1 + 2 + 3 = 6,这就是我们的结果。

哇。最开始几次你见到fold的时候可能觉得有点奇怪,不过一旦你习惯了它,你就会在到处都用它。任何时候你有一个列表,然后你需要一个单一的结果,fold就是合适的。

消费者很重要还因为另一个我们没有讨论到的迭代器的属性:惰性。让我们更多的讨论一下迭代器,你就知道为什么消费者重要了。

迭代器(Iterators)

正如我们之前说的,迭代器是一个我们可以重复调用它的.next()方法,然后它会给我们一个数据序列的结构。因为你需要调用函数,这意味着迭代器是惰性的lazy )并且不需要预先生成所有的值。例如,下面的代码并没有真正的生成1-99这些数,而是创建了一个值来代表这个序列:

let nums = 1..100;

因为我们没有用范围做任何事,它并未生成序列。让我们加上消费者:

let nums = (1..100).collect::<Vec<i32>>();

现在,collect()会要求范围生成一些值,接着它会开始产生序列。

范围是你会见到的两个基本迭代器之一。另一个是iter()iter()可以把一个向量转换为一个简单的按顺序给出每个值的迭代器:

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

for num in nums.iter() {
   println!("{}", num);
}

这两个基本迭代器应该能胜任你的工作。还有一些高级迭代器,包括一个是无限的。

关于迭代器的介绍足够了。迭代适配器是关于迭代器最后一个要介绍的内容了。让我们开始吧!

迭代适配器(Iterator adapters)

迭代适配器Iterator adapters)获取一个迭代器然后按某种方法修改它,并产生一个新的迭代器。最简单的是一个是map

(1..100).map(|x| x + 1);

在其他迭代器上调用map,然后产生一个新的迭代器,它的每个元素引用被调用了作为参数的闭包。所以它会给我们2-100这些数字。好吧,看起来是这样。如果你编译这个例子,你会得到一个警告:

warning: unused result which must be used: iterator adaptors are lazy and
         do nothing unless consumed, #[warn(unused_must_use)] on by default
(1..100).map(|x| x + 1);
 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

又是惰性!那个闭包永远也不会执行。这个例子也不会打印任何数字:

(1..100).map(|x| println!("{}", x));

如果你尝试在一个迭代器上执行带有副作用的闭包,不如直接使用for

有大量有趣的迭代适配器。take(n)会返回一个源迭代器下n个元素的新迭代器,注意这对源迭代器没有副作用。让我们试试我们之前的无限迭代器,count()

for i in (1..).take(5) {
    println!("{}", i);
}

这会打印:

1
2
3
4
5

filter()是一个带有一个闭包参数的适配器。这个闭包返回truefalsefilter()返回的新迭代器只包含闭包返回true的元素:

for i in (1..100).filter(|&x| x % 2 == 0) {
    println!("{}", i);
}

这会打印出 1 到 100 之间所有的偶数。(注意:不像map,传递给filter闭包传递了一个元素的引用而不是元素本身。这里定义的过滤器使用&x模式来提取整型。过滤器闭包传递的是一个引用因为它返回truefalse而不是元素,所以过滤器的实现必须保持元素的所有权并传递给新创建的迭代器。)

(注意因为filter并不消费它迭代的元素,它传递每个元素的引用,所以过滤器使用&x来提取其中的整型数据。)

你可以链式的调用所有三种结构:以一个迭代器开始,适配几次,然后处理结果。看看下面的:

(1..)
    .filter(|&x| x % 2 == 0)
    .filter(|&x| x % 3 == 0)
    .take(5)
    .collect::<Vec<i32>>();

这会给你一个包含612182430的向量。

这只是一个迭代器、迭代适配器和消费者如何帮助你的小尝试。有很多非常实用的迭代器,当然你也可以编写你自己的迭代器。迭代器提供了一个安全、高效的处理所有类型列表的方法。最开始它们显得比较不寻常,不过如果你玩转了它们,你就会上瘾的。关于不同迭代器和消费者的列表,查看迭代器模块文档