4.5.迭代器

最后更新于:2022-04-01 00:43:11

让我们讨论一下循环。 还记得Rust的`for`循环吗?这是一个例子: ~~~ for x in 0..10 { println!("{}", x); } ~~~ 现在我们更加了解Rust了,我们可以谈谈这里的具体细节了。这个范围(`0..10`)是“迭代器”。我们可以重复调用迭代器的`.next()`方法,然后它会给我们一个数据序列。 就像这样: ~~~ let mut range = 0..10; loop { match range.next() { Some(x) => { println!("{}", x); }, None => { break } } } ~~~ 我们创建了一个`range`的可变绑定,它是我们的迭代器。我们接着`loop`,它包含一个`match`。`match`用来匹配`range.next()`的结果,它给我们迭代器的下一个值。`next`返回一个`Option`,在这个例子中,它会返回`Some(i32)`如果有值然后返回`None`当我们循环完毕。如果我们得到`Some(i32)`,我们打印它,如果我们得到`None`,我们`break`出循环。 这个代码例子基本上和我们的`loop`版本一样。`for`只是`loop/match/break`结构的简便写法。 然而,`for`循环并不是唯一使用迭代器的结构。编写你自己的迭代器涉及到实现`Iterator`特性。然而特性不是本章教程的涉及范围,不过Rust提供了一系列的有用的迭代器帮助我们完成各种任务。在我们开始讲解之前,我们需要看看一个Rust的反面模式。这就是如此使用范围。 是的,我们刚刚谈论到范围是多么的酷。不过范围也是非常原始的。例如,如果你想迭代一个向量的内容,你可能尝试这么写: ~~~ let nums = vec![1, 2, 3]; for i in 0..nums.len() { println!("{}", nums[i]); } ~~~ 这严格的说比使用现成的迭代器还要糟。你可以直接在向量上迭代。所以这么写: ~~~ 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_)。下面是一些定义: * _迭代器_给你一个值的序列 * _迭代适配器_操作迭代器,产生一个不同输出序列的新迭代器 * _消费者_操作迭代器,产生最终值的集合 让我们先看看消费者,因为我们已经见过范围这个迭代器了。 ## 消费者 _消费者_操作一个迭代器,返回一些值或者几种类型的值。最常见的消费者是`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`类型”。为此`_`有事被称为“类型占位符”。 `collect()`是最常见的消费者,不过这还有其它的消费者。`find()`就是一个: ~~~ let greater_than_forty_two = (0..100) .find(|x| *x > 42); match greater_than_forty_two { Some(_) => println!("We got some numbers!"), None => println!("No numbers 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_)。每次迭代,这个闭包都会被调用,返回值是下一次迭代的累计数。在我们的第一次迭代,基数是累计数。 好吧,这有点混乱。让我们检查一下这个迭代器中所有这些值: | 基数 | 累计数 | 元素 | 闭包结果 | | --- | --- | --- | --- | | 0 | 0 | 1 | 1 | | 0 | 1 | 2 | 3 | | 0 | 3 | 3 | 6 | 我们可以使用这些参数调用`fold()`: ~~~ .fold(0, |sum, x| sum + x); ~~~ 那么,`0`是我们的基数,`sum`是累计数,`x`是元素。在第一次迭代,我们设置`sum`为`0`,然后`x`是`nums`的第一个元素,`1`。我们接着把`sum`和`x`相加,得到`0 + 1 = 1`。在我们第二次迭代,`sum`成为我们的累计值,元素是数组的第二个值,`2`,`1 + 2 = 3`,然后它就是最后一次迭代的累计数。在这次迭代中,`x`是最后的元素,`3`,那么`3 + 3 = 6`,就是我们和的最终值。`1 + 2 + 3 = 6`,这就是我们的结果。 (口哨)。最开始你见到`fold`的时候可能觉得有点奇怪,不过一旦你习惯了它,你就会在到处都用它。任何时候你有一个列表,然后你需要一个单一的结果,`fold`就是合适的。 消费者很重要还因为另一个我们没有讨论到的迭代器的属性:惰性。让我们更多的讨论一下迭代器,你就知道为什么消费者重要了。 ## 迭代器 正如我们之前说的,迭代器是一个我们可以重复调用它的`.next()`方法,然后它会给我们一个数据序列的结构。因为你需要调用函数,这意味着迭代器是_懒惰_(_lazy _)的并且不需要预先生成所有的值。例如,下面的代码并没有真正的生成`1-100`这些数,而是创建了一个值来代表这个序列: ~~~ let nums = 1..100; ~~~ 因为我们没有用范围做任何事,它并生成序列。让我们加上消费者: ~~~ let nums = (1..100).collect::<Vec<i32>>(); ~~~ 现在,`collect()`会要求范围生成一些值,接着它会开始产生序列。 范围是你会见到的两个基本迭代器之一。另一个是`iter()`。`iter()`可以把一个向量转换为一个简单的按顺序给出每个值的迭代器: ~~~ let nums = [1, 2, 3]; for num in nums.iter() { println!("{}", num); } ~~~ 这两个基本迭代器应该能胜任你的工作。这还有一些高级迭代器,包括一个是无限的。像`count`: ~~~ std::iter::count(1, 5); ~~~ 这个迭代器从1开始计数,每次加5.它每次都会给你一个新值,直到永远(好吧,从技术上讲直到它循环到`i32`所能代表的最大值)。不过因为它是懒惰的,这没有问题!你可能不会想在它之上使用`collect()`。 足够关于迭代器的知识了。迭代适配器是关于迭代器最后一个要介绍的内容了。让我们开始吧! ## 迭代适配器(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 std::iter::count(1, 5).take(5) { println!("{}", i); } ~~~ 这会打印: ~~~ 1 6 11 16 21 ~~~ `filter()`是一个带有一个闭包参数的适配器。这个闭包返回`true`或`false`。`filter()`返回的新迭代器只包含闭包返回`true`的元素: ~~~ for i in (1..100).filter(|&x| x % 2 == 0) { println!("{}", i); } ~~~ 这会打印出1到100之间所有的偶数。(注意因为`filter`并不消费它迭代的元素,它传递每个元素的引用,所以过滤器使用`&x`来获取其中的整形数据。) 你可以链式的调用所有三种结构:以一个迭代器开始,适配几次,然后处理结果。看看下面的: ~~~ (1..1000) .filter(|&x| x % 2 == 0) .filter(|&x| x % 3 == 0) .take(5) .collect::<Vec<i32>>(); ~~~ 这会给你一个包含`6`,`12`,`18`,`24`和`30`的向量。 这只是一个迭代器,迭代适配器和消费者如何帮助你的小尝试。这里有很多非常实用的迭代器,当然你也可以编写你自己的迭代器。迭代器提供了一个安全,高效的处理所有类型列表的方法。最开始它们显得比较不寻常,不过如果你玩转了它们,你就会上瘾的。关于不同迭代器和消费者的列表,查看[迭代器模块文档](http://doc.rust-lang.org/std/iter/)。
';