Rust 从 0 到 1- 函数式编程 - 迭代器
迭代器模式主要用于对一组数据的顺序操作。迭代器(iterator)负责实现遍历所有数据的逻辑以及决定什么时候遍历结束。因此,当使用迭代器时,我们无需自己重新实现对数据集合的遍历逻辑。
在 Rust 中,迭代器是“惰性”(lazy)的,即在实际执行遍历之前它不会产生任何效果或影响。参考下面的例子:
一旦我们创建了迭代器,可以通过多种方式使用它。譬如,使用 for 循环进行遍历(前面讨论控制流的例子中我们使用了 for 循环对数组进行遍历并打印其中的元素)。参考下面的例子:
在没有提供迭代器的语言中,我们可能需要使用下标从 0 开始对数据集合进行遍历,并循环增加下表的值,直到和元素的数量相等。迭代器为我们处理了这些逻辑,这减少了重复代码并消除了可能的潜在问题。此外,迭代器也更加灵活,除了可以实现对类似 vector 这种可以通过索引访问的数据结构的遍历以外,还可以使用相同的方式对很多种不同的数据集合进行遍历。下面让我们看看迭代器是如何做到的。
Iterator trait 和 next 方法
所有的迭代器都实现了定义于标准库中的 Iterator trait。它的定义看起来类似下面这样:
这里有两个还未讨论到的新语法:type Item 和 Self::Item,他们定义了 trait 的关联类型(associated type),后面我们会对此进行更为详细的讨论,目前我们只需知道实现 Iterator trait 的同时要求定义一个 Item 类型,这个 Item 类型被用作 next 方法的返回值类型。也就是说,Item 类型就是迭代器返回的元素的类型。Iterator trait 只要求实现 next 一个方法,其每次被调用会返回其中一个元素,并被包装在 Some 中,当遍历结束时,返回 None。我们可以直接调用迭代器的 next 方法,参考下面的例子:
注意 v1_iter 是可变的:这是因为调用 next 方法会改变迭代器中用来记录序列位置的状态。我们也可以理解为,代码对迭代器进行了消费(consumes or uses up ),每次调用 next 都会消耗掉迭代器中的一个元素。在使用 for 循环时不需要 v1_iter 可变,是因为 for 循环会获取 v1_iter 的所有权并在其内部让 v1_iter 可变。
另外,需要注意我们在例子中调用 next 方法获得的是 vector 中包含的值的不可变引用,这是因为 iter 方法生成的是一个不可变引用的迭代器。如果我们希望获取所有权,需要调用 into_iter 方法;如果我们希望获得可变引用,则需要调用 iter_mut 方法。
消费(Consume)迭代器的方法
Iterator trait 中还包含很多由标准库提供了默认实现的方法,我们可以在 Iterator trait 的 API 文档中进行查阅。其中一些方法会调用 next 方法,这也是为什么在实现 Iterator trait 时要求要实现 next 方法的原因。
这些调用 next 方法的方法被称为消费适配器(consuming adaptors),因为调用他们会耗尽迭代器。以 sum 方法为例,这个方法会获取迭代器的所有权并反复调用 next 方法进行遍历,将所有项进行累加并在遍历完成时返回总和。参考下面的例子:
注意,调用 sum 方法之后,由于它会获取迭代器的所有权,v1_tier 方法将无法再使用。
生成其它迭代器的方法
Iterator trait 中还有另一类方法,被称为迭代器适配器(iterator adaptors),我们可以利用它将当前迭代器变为不同类型的迭代器,还可以进行链式调用。不过因为迭代器都是惰性的,我们只有在调用一个消费适配器方法时才会获得迭代器适配器的结果。参考下面的例子:
上面的例子中我们调用了一个迭代器适配器 map 方法,其使用闭包来生成了一个新的迭代器,其会对 v1 中的每个元素都被加 1。但是如果我们在编译时会收到类似下面的警告:
由于我们只调用了迭代器适配器,其所包含的闭包从未被调用过。从编译的提示信息我们也可以看到:iterators are lazy and do nothing unless consumed。
为了解决这个问题并消费迭代器,我们将使用 collect 方法,它会消费迭代器(在例子中就是对每个元素执行闭包中的逻辑,将每个元素加 1)并将结果存储到集合类型数据结构中。参考下面的例子:
因为 map 使用了闭包,我们可以在遍历时对每个元素执行的任意的操作。这是一个展示如何在遍历时使用闭包来实现自定义行为的非常好的例子。
使用闭包获取环境
下面让我们展示一个使用迭代器适配器 filter 和闭包的例子,我们会在闭包中获取其所在的环境(作用域中的变量)。 filter 方法会进行遍历并判断闭包的返回值,如果返回值是 true,元素将会包含在的新迭代器中,如果返回值是 false,元素将会被过滤掉,不会包含在新的迭代器中。参考下面的例子,我们将遍历一个 Shoe 结构体集合并只返回指定大小的鞋子:
上面的例子中 shoes_in_size 函数体中调用 into_iter 创建了一个获取参数 shoes 所有权的迭代器;接着调用 filter 将这个迭代器变为一个只包含“通过闭包计算返回 true ”的元素的新迭代器;闭包从作用域中获取 shoe_size 变量并将其与 shoes 中鞋的大小作比较,只在相等时返回 true,这意味着 shoes 中与指定大小不同的鞋子将被过滤掉。最后,调用 collect 方法获取结果并返回。建议大家可以尝试运行一下上面的例子。
创建自定义迭代器
我们可以在 vector 上调用 iter、into_iter 或 iter_mut 来创建一个迭代器,也可以使用标准库中其他的集合类型创建迭代器,如 hashmap。除此之外,我们可以通过实现 Iterator trait 来创建我们自定义的迭代器,并且就像前面我们说的,唯一的要求就是实现 next 方法,之后,我们就可以使用其他由 Iterator trait 默认实现的所有方法!
下面,我们将会创建一个从 1 到 5 进行计数的迭代器来作为例子。首先让我们创建一个自定义的结构体用于存放计数:
接下来我们为 Counter 实现 Iterator trait,并实现 next 方法自定义迭代器的行为:
上面的例子中,我们将迭代器的关联类型 Item 设置为 u32,这代表迭代器的返回值为 u32 类型,并且当 count 大于或等于 6 时,next 会返回 None,迭代结束。
使用我们自定义的迭代器 next 方法
下面我们通过一个直接调用 next 方法的测试,来展示使用 Counter 的迭代器功能:
在上面的例子中我们新进了一个 Counter 实例,接着反复调用 next 方法,并验证其行为符是否符合我们的预期。
使用 Iterator trait 中的其它方法
因为我们已经实现了 Iterator trait 所必须的 next 方法,现在可以使用 Iterator trait 中的任何拥有默认实现的其它方法。譬如,假设我们希望将 Counter 实例产生的值与另一个跳过第一个值的 Counter 实例的值进行配对,接着将每一对值相乘,然后只保留可以被三整除的值,最后将所有的值相加:
注意,zip 只产生四对值。这是因为于第二个 Counter 实例产生的第五个值是 None,而 zip 在任一迭代器返回 None 时也返回 None,迭代结束。
版权声明: 本文为 InfoQ 作者【山】的原创文章。
原文链接:【http://xie.infoq.cn/article/e01f5b04a392239d82cbf8174】。文章转载请联系作者。
评论