卑微的循环经历了一段越来越便利和越来越抽象的漫长旅程。B语言(C的前身)只有while (condition) { ... }
,但随着C的到来,通过数组索引进行遍默的常见场景随着for
循环的添加而变得更加方便:
// C code
int i;
for (i = 0; i < len; i++) {
Item item = collection[i];
// body
}
C++的早期版本允许将循环变量声明嵌入for
语句中,从而进一步提高了便利性和范围界定(C在C99中也采用了这一点):
// C++98 code
for (int i = 0; i < len; i++) {
Item item = collection[i];
// ...
}
大多数现代语言进一步抽象了循环的概念:循环的核心功能通常是移动到某个容器的下一个项目,跟踪到达该项目所需的物流(index++
或++it
)大多是一个无关紧要的细节。这一实现产生了两个核心概念:
- iterators:一种类型,其目的是重复发射容器1的下一个项目,直到用尽。
- For-Each Loops:一个紧凑的循环表达式,用于遍及容器中的所有项目,将循环变量绑定到该项目,而不是到达该项目的详细信息。
这些概念允许更短的循环代码,并且(更重要的是)更清楚地说明了什么意图:
// C++11 code
for (Item& item : collection) {
// ...
}
一旦这些概念可用,它们显然非常强大,以至于它们被迅速改装成那些还没有它们的语言(例如,每个循环都被添加到Java1.5和C++11中)。
Rust包括整数和for-a每个样式循环,但它也包括抽象的下一步:允许整个循环表示为整数变换。与改进rust代码的35种具体方法-类型(三)对Option
和Result
的讨论一样,该项目将尝试展示如何使用这些回数变换而不是显式循环,并指导何时是一个好主意。
在本项结束时,一个类似C的显式循环将向量的前五个偶数项的平方相加:
let mut even_sum_squares = 0;
let mut even_count = 0;
for i in 0..values.len() {
if values[i] % 2 != 0 {
continue;
}
even_sum_squares += values[i] * values[i];
even_count += 1;
if even_count == 5 {
break;
}
}
应该开始感觉更自然地表达为功能性风格表达:
let even_sum_squares: u64 = values
.iter()
.filter(|x| *x % 2 == 0)
.take(5)
.map(|x| x * x)
.sum();
像这样的惯量转换表达式大致可以分为三部分:
- 初始源更像器,来自Rust的其它者特征之一。
- 一系列的iterator变换。
- 将迭代结果组合成最终值的最终消费者方法。
前两个有效地将功能从循环主体移动到for
表达式中;最后一个完全消除了对for
语句的需求。
遍历特质(迭代特质)
核心Iterator特征有一个非常简单的界面:next方法,产生Some
项目,直到它没有(None
)。
允许对其内容进行迭代的集合(称为迭代对象)实现了IntoIterator特征;该特征的into_iter方法消耗了Self
,并代替其发出一个Iterator
。编译器将自动使用此特征来表达表单
for item in collection {
// body
}
有效地将它们转换为代码,大致如下:
let mut iter = collection.into_iter();
loop {
let item: Thing = match iter.next() {
Some(item) => item,
None => break,
};
// body
}
或更简洁、更惯用:
let mut iter = collection.into_iter();
while let Some(item) = iter.next() {
// body
}
(为了保持事情顺利进行,还有适用于任何Iterator
的IntoIterator
的实现,它只是返回self
;毕竟,将Iterator
转换为Iterator
很容易!)
这个初始形式是一个消耗的iterator,在创建时用尽了集合:
let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
for item in collection {
println!("Consumed item {:?}", item);
}
任何在被遍复使用集合的尝试都失败了:
println!("Collection = {:?}", collection);
error[E0382]: borrow of moved value: `collection`
--> iterators/src/main.rs:156:35
|
148 | let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
| ---------- move occurs because `collection` has type `Vec<Thing>`, which does not implement the `Copy` trait
149 | for item in collection {
| ----------
| |
| `collection` moved due to this implicit call to `.into_iter()`
| help: consider borrowing to avoid moving into the for loop: `&collection`
...
156 | println!("Collection = {:?}", collection);
| ^^^^^^^^^^ value borrowed here after move
|
note: this function takes ownership of the receiver `self`, which moves `collection`
虽然简单易懂,但这种全消耗的行为往往是不可取的;需要借用某种形式的重新项目。
为了确保行为清晰,这里的示例使用不是Copy
Item
类型(改进rust代码的35种具体方法-类型(五)-熟悉标准特征),因为这将隐藏所有权问题-编译器会无声地在任何地方进行复制。
// Deliberately not `Copy`
#[derive(Clone, Debug, Eq, PartialEq)]
struct Thing(u64);
let collection = vec![Thing(0), Thing(1), Thing(2), Thing(3)];
如果正在遍默的集合以&
为前缀:
for item in &collection {
println!("{}", item.0);
}
println!("collection still around {:?}", collection);
然后,Rust编译器将为类型&Collection
寻找IntoIterator的实现。正确设计的集合类型将提供这样的实现;该实现仍将消耗Self
,但现在Self
是&Collection
而不是Collection
,并且相关的Item
类型将是参考&Thing
。
这使得迭代后的集合完好无损,等效的扩展代码是:
let mut iter = (&collection).into_iter();
while let Some(item) = iter.next() {
println!("{}", item.0);
}
如果在可变引用2上提供迭代是有意义的,那么类似的模式适用于for item in &mut collection
:编译器为&mut Collection
查找并使用IntoIterator
的实现,每个Item
都是&mut Thing
类型。
按照惯例,标准容器还提供了一个iter()
方法,该方法通过对基础项的引用返回一个遍历表,并在适当的情况下等同于iter_mut()
方法,其行为与刚才描述的行为相同。这些方法可用于循环,但当用作回数转换的开始时,具有更明显的好处:
let result: u64 = (&collection).into_iter().map(|thing| thing.0).sum();
修改为:
let result: u64 = collection.iter().map(|thing| thing.0).sum();
iterator变换
Iterator特征具有单个必需方法(next),但也提供了大量其他方法的默认实现(项目13),这些方法在遍历器上执行转换。
其中一些转换会影响整个迭代过程:
- take(n)限制一个更多的一个它来发射
n
个项目。 - skip(n)跳过遍遍位数的前
n
元素。 - step_by(n)转换一个iterator,因此它只发出第n个项目。
- chain(other)将两个遍构件粘在一起,以构建一个组合的遍构,然后穿过一个。
- cycle()将终止的回复器转换为永远重复的回复器,每当它到达终点时,从头开始。(它必须支持
Clone
才能允许这样做。) - rev()颠倒了一个反转器的方向。(回历器必须实现DoubleEndedIterator特征,该特征具有额外的next_back必需方法。)
其他转换会影响作为Iterator
主题Item
的性质:
- map(|item| {...})是最通用的版本,反复应用闭包来依次转换每个项目。此列表中的以下几个条目是方便变体,可以等效地作为
map
实现。 - cloned()生成原始重复器中所有项目的克隆;这对于
&Item
引用上的重复器特别有用。(这显然需要底层Item
类型来实现Clone
)。 - copied()生成原始遍位器中所有项的副本;这对
&Item
引用的位参与位器特别有用。(这显然需要底层Item
类型来实现Copy
)。 - enumerate()将项目上的遍合器转换为
(usize, Item)
对的遍/位器,为遍/或者中的项目提供索引。 - zip(it)将一个更像器与第二个更像器连接起来,产生一个组合的更像器,从每个原始的更像器中发出一对项目,直到两个较短的一个完成。
然而,其他转换对Iterator
发出的项目进行过滤:
- filter(|item| {...})是最通用的版本,对每个项目引用应用
bool
返回闭包,以确定是否应该通过。 - take_while()和skip_while()是彼此的镜像,基于谓词发出初始子范围或其初始子范围或最终子范围。
flatten()方法处理一个iterator,其项目本身就是iterators,使结果变平。就其本身而言,这似乎没有那么有帮助,但当与Option和Result都充当iterators的观察相结合时,它变得更加有用:它们产生零(forNone,Err(e)
)或一个(for Some(v)
Ok(v)
)项目。这意味着flatten
Option
/Result
值流是仅提取有效值而忽略其余值的简单方法。
总的来说,这些方法允许对一体进行转换,以便它们准确地生成大多数情况下所需的元素序列。
Iterator消费者
前两节描述了如何获得迭代器,以及如何将其转换为精确迭代的正确形式。这种精确的目标迭代可以作为显式的每个循环发生:
let mut even_sum_squares = 0;
for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
even_sum_squares += value * value;
}
然而,Iterator方法的大量集合包括许多允许在单个方法调用中使用迭代的方法,消除了对显式循环的需求。
这些方法中最通用的是for_each(|item| {...})它为Iterator
生成的每个项目运行闭包。这可以做显式for
循环可以做的大多数事情(例外情况如下所述),但其通用性也使其使用起来有点尴尬——闭包需要使用对外部状态的可变引用才能发出任何内容:
let mut even_sum_squares = 0;
values
.iter()
.filter(|x| *x % 2 == 0)
.take(5)
.for_each(|value| {
// closure needs a mutable reference to state elsewhere
even_sum_squares += value * value;
});
然而,如果for
循环的主体与一些常见模式之一匹配,那么有更具体的更清晰、更短、更惯用的惯用方法。
这些模式包括从集合中构建单个值的快捷方式:
- sum(),用于求和数值(整数或浮点数)的集合。
- product(),用于将数值集合相乘。
- min()和max()用于查找相对于
Item
Ord
实现的集合的极端值(见项目5)。 - min_by(f)和max_by(f)用于查找集合的极端值,相对于用户指定的比较函数
f
。 - reduce(f)是一个更通用的操作,包括以前的方法,通过在每个步骤运行一个闭包来构建
Item
类型的累积值,该闭包将迄今为止的累积值和当前项目。 - fold(f)是
reduce
的推广,允许“累积值”为任意类型(不仅仅是Iterator::Item
类型)。 - scan(f)以略微不同的方式推广,使闭包在每一步都对一些内部状态进行可变引用。
还有从集合中选择单个值的方法:
- find(p)找到满足谓词的第一个项目。
- position(p)也找到满足谓词的第一个项目,但这次它返回该项目的索引。
- nth(n)返回回复器的第
n
个元素(如果可用)。
有一些方法可以对集合中的每个项目进行测试:
(无论哪种情况,如果发现相关的反例,迭代都将提前终止。)
有一些方法允许在每个项目使用的闭包中出现故障的可能性;在每种情况下,如果闭包返回一个项目的故障,则迭代被终止,整个操作返回第一个故障。
- try_for_each(f)就像
for_each
一样,但关闭可能会失败。 - try_fold(f)就像
fold
一样,但关闭可能会失败。 - try_find(f)就像
find
一样,但关闭可能会失败。
最后,有一些方法将所有经过的累进项目累积到一个新的集合中。其中最重要的是collect()它可用于构建实现FromIterator特征的任何集合类型的新实例。
FromIterator
特征是针对所有标准库集合类型(Vec、HashMap、BTreeSet等)实现的,但这种普遍性也意味着您经常必须使用显式类型,否则编译器无法确定您是试图组装(例如)Vec<i32>
还是HashSet<i32>
。
use std::collections::HashSet;
// Build collections of even numbers. Type must be specified, because
// the expression is the same for either type.
let myvec: Vec<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
let h: HashSet<i32> = (0..10).into_iter().filter(|x| x % 2 == 0).collect();
(顺便说一句,这个例子还说明了使用范围表达式来生成要遍及的初始数据。)
其他(更晦涩的)收藏生产方法包括:
- unzip(),它将成对的iterator分为两个集合。
- partition(p),它根据应用于每个项目的谓词将一个反演符拆分为两个集合。
本项目涉及了广泛的Iterator
方法选择,但这只是可用方法的一个子集;有关更多信息,请参阅Iterator文档或阅读Programming Rust(第二版)第15章,该章涵盖了广泛的可能性。
这个丰富的惯用器转换集合旨在被使用——以产生更惯用、更紧凑、意图更清晰的代码。
从Result值构建集合
上一节描述了使用collect()
从重述器构建集合,但collect()
在处理Result
值时也有特别有用的功能。
考虑尝试将i64
值的向量转换为字节(u8
),乐观地期望它们都适合:
use std::convert::TryFrom;
let inputs: Vec<i64> = vec![0, 1, 2, 3, 4];
let result: Vec<u8> = inputs
.into_iter()
.map(|v| <u8>::try_from(v).unwrap())
.collect();
这有效,直到出现一些意想不到的输入:
let inputs: Vec<i64> = vec![0, 1, 2, 3, 4, 512];
并导致运行时故障:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: TryFromIntError(())', iterators/src/main.rs:249:36
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
根据改进rust代码的35种具体方法-类型(三)的建议,我们希望保持Result
类型,并使用?
操作员使任何故障成为调用代码的问题。发出Result
的明显修改并没有真正帮助:
let result: Vec<Result<u8, _>> =
inputs.into_iter().map(|v| <u8>::try_from(v)).collect();
// Now what? Still need to iterate to extract results and detect errors.
然而,有一个替代版本的collect()
它可以组装一个持有Vec
Result
,而不是一个持有结果的Vec
。
强制使用此版本需要涡轮鱼(::<Result<Vec<_>, _>>
):
let result: Vec<u8> = inputs
.into_iter()
.map(|v| <u8>::try_from(v))
.collect::<Result<Vec<_>, _>>()?;
将此与问号运算符相结合会产生有用的行为:
- 如果迭代遇到错误值,该错误值将发送到调用者,迭代停止。
- 如果没有遇到错误,代码的其余部分可以处理正确类型的合理值集合。
循环转换
此项目的目的是说服您,许多显式循环可以被视为转换为重子变换的东西。对于不习惯的程序员来说,这可能会感觉有些不自然,所以让我们一步一步地进行转变。
从一个非常类似C的显式循环开始,将向量前五个偶数项的平方相加:
// C code
let mut even_sum_squares = 0;
let mut even_count = 0;
for i in 0..values.len() {
if values[i] % 2 != 0 {
continue;
}
even_sum_squares += values[i] * values[i];
even_count += 1;
if even_count == 5 {
break;
}
}
第一步是用在for-al循环中直接使用一个回率来替换向量索引:
let mut even_sum_squares = 0;
let mut even_count = 0;
for value in values.iter() {
if value % 2 != 0 {
continue;
}
even_sum_squares += value * value;
even_count += 1;
if even_count == 5 {
break;
}
}
使用continue
跳过某些项目的循环初始臂自然表示为afilterfilter()
let mut even_sum_squares = 0;
let mut even_count = 0;
for value in values.iter().filter(|x| *x % 2 == 0) {
even_sum_squares += value * value;
even_count += 1;
if even_count == 5 {
break;
}
}
接下来,一旦发现5个偶数项目,就提前退出循环,映射到take(5)
:
let mut even_sum_squares = 0;
for value in values.iter().filter(|x| *x % 2 == 0).take(5) {
even_sum_squares += value * value;
}
项目的值从不直接使用,仅用于value * value
组合,这使其成为map()
的理想目标:
let mut even_sum_squares = 0;
for val_sqr in values.iter().filter(|x| *x % 2 == 0).take(5).map(|x| x * x)
{
even_sum_squares += val_sqr;
}
原始循环的这些重构产生了一个循环体,这是适合在sum()
方法:
let even_sum_squares: u64 = values
.iter()
.filter(|x| *x % 2 == 0)
.take(5)
.map(|x| x * x)
.sum();
更好解决方式
本项目强调了重转换的优点,特别是在简洁和清晰方面。那么,什么时候惯用器变换不合适或惯用?
- 如果循环体是大和/或多功能的,那么将其保留为显式体而不是将其挤压到闭包是有意义的。
- 如果循环主体涉及导致周围函数提前终止的错误条件,这些通常最好保持明确性——
try_..()
方法只会有一点帮助。然而,collect()
将Result
值的集合转换为持有值集合Result
的能力通常允许仍然用?
处理错误条件。操作员。 - 如果性能至关重要,则涉及闭包的重现器转换应该得到优化,使其与等效的显式代码一样快。但是,如果核心循环的性能如此重要,请测量不同的变体并进行适当调整。
- 小心确保您的测量反映现实世界的性能——编译器的优化器可以对测试数据给出过于乐观的结果(如第30项所述)。
- Godbolt编译器浏览器是探索编译器吐出内容的惊人工具。
最重要的是,如果转换是被迫的或尴尬的,不要将循环转换为迭代转换。这是一个品味问题,可以肯定的是——但请注意,随着您越来越熟悉功能风格,您的品味可能会发生变化。
1:事实上,它可以更通用——在完成之前发出下一个项目的想法不需要与容器相关联。
2:如果项目突变可能会使容器的内部保证无效,则无法提供此方法。例如,以改变其Hash值的方式更改项目的内容将使HashMap
的内部数据结构无效。