迭代器

ch13-02-iterators.md
commit 3f2a1bd8dbb19cc48b210fc4fb35c305c8d81b56

迭代器是 Rust 中的一个模式,它允许你对一个项的序列进行某些处理。例如。列表 13-5 中对 vecctor 中的每一个数加一:

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

let v2: Vec<i32> = v1.iter().map(|x| x + 1).collect();

assert_eq!(v2, [2, 3, 4]);

Listing 13-5: Using an iterator, map, and collect to add one to each number in a vector

vector 的iter方法允许从 vector 创建一个迭代器iterator)。接着迭代器上的map方法调用允许我们处理每一个元素:在这里,我们向map传递了一个对每一个元素x加一的闭包。map是最基本的与比较交互的方法之一,因为依次处理每一个元素是非常有用的!最后collect方法消费了迭代器并将其元素存放到一个新的数据结构中。在这个例子中,因为我们指定v2的类型是Vec<i32>collect将会创建一个i32的 vector。

map这样的迭代器方法有时被称为迭代器适配器iterator adaptors),因为他们获取一个迭代器并产生一个新的迭代器。也就是说,map在之前迭代器的基础上通过调用传递给它的闭包来创建了一个新的值序列的迭代器。

概括一下,这行代码进行了如下工作:

  1. 从 vector 中创建了一个迭代器。
  2. 使用map适配器和一个闭包参数对每一个元素加一。
  3. 使用collect适配器来消费迭代去并生成了一个新的 vector。

这就是如何产生结果[2, 3, 4]的。如你所见,闭包是使用迭代器的很重要的一部分:他们提供了一个自定义类似map这样的迭代器适配器的行为的方法。

迭代器是惰性的

在上一部分,你可能已经注意到了一个微妙的用词区别:我们说map适配adapts)了一个迭代器,而collect消费consumes)了一个迭代器。这是有意为之的。单独的迭代器并不会做任何工作;他们是惰性的。也就是说,像列表 13-5 的代码但是不调用collect的话:

let v1: Vec<i32> = vec![1, 2, 3];

v1.iter().map(|x| x + 1); // without collect

这可以编译,不过会给出一个警告:

warning: unused result which must be used: iterator adaptors are lazy and do
nothing unless consumed, #[warn(unused_must_use)] on by default
 --> src/main.rs:4:1
  |
4 | v1.iter().map(|x| x + 1); // without collect
  | ^^^^^^^^^^^^^^^^^^^^^^^^^

这个警告是因为迭代器适配器实际上并不自己进行处理。他们需要一些其他方法来触发迭代器链的计算。我们称之为消费迭代器consuming adaptors),而collect就是其中之一。

那么如何知道迭代器方法是否消费了迭代器呢?还有哪些适配器是可用的呢?为此,让我们看看Iterator trait。

Iterator trait

迭代器都实现了一个标准库中叫做Iterator的 trait。其定义看起来像这样:

trait Iterator {
    type Item;

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

这里有一些还未讲到的新语法:type ItemSelf::Item定义了这个 trait 的关联类型associated type),第XX章会讲到关联类型。现在所有你需要知道就是这些代码表示Iterator trait 要求你也定义一个Item类型,而这个Item类型用作next方法的返回值。换句话说,Item类型将是迭代器返回的元素的类型。

让我们使用Iterator trait 来创建一个从一数到五的迭代器Counter。首先,需要创建一个结构体来存放迭代器的当前状态,它有一个u32的字段count。我们也定义了一个new方法,当然这并不是必须的。因为我们希望Counter能从一数到五,所以它总是从零开始:

struct Counter {
    count: u32,
}

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

接下来,我们将通过定义next方法来为Counter类型实现Iterator trait。我们希望迭代器的工作方式是对当前状态加一(这就是为什么将count初始化为零,这样迭代器首先就会返回一)。如果count仍然小于六,将返回当前状态,不过如果count大于等于六,迭代器将返回None,如列表 13-6 所示:

# struct Counter {
#     count: u32,
# }
#
impl Iterator for Counter {
    // Our iterator will produce u32s
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // increment our count. This is why we started at zero.
        self.count += 1;

        // check to see if we've finished counting or not.
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}

Listing 13-6: Implementing the Iterator trait on our Counter struct

type Item = u32这一行表明迭代器中Item的关联类型将是u32。同样无需担心关联类型,因为第XX章会涉及他们。

next方法是迭代器的主要接口,它返回一个Option。如果它是Some(value),相当于可以迭代器中获取另一个值。如果它是None,迭代器就结束了。在next方法中可以进行任何迭代器需要的计算。在这个例子中,我们对当前状态加一,接着检查其是否仍然小于六。如果是,返回Some(self.count)来产生下一个值。如果大于等于六,迭代结束并返回None

迭代器 trait 指定当其返回None,就代表迭代结束。该 trait 并不强制任何在next方法返回None后再次调用时必须有的行为。在这个情况下,在第一次返回None后每一次调用next仍然返回None,不过其内部count字段会依次增长到u32的最大值,接着count会溢出(在调试模式会panic!而在发布模式则会折叠从最小值开始)。