Iterator 和 IntoIterator
迭代器是指任意实现了 std::iter::Iterator
trait 的值。
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
... // 其它 default 方法
}
其中 Item
是迭代器每次产生的值的类型。next
方法要么返回 Some(v)
(其中 v 是迭代器的下一个值),要么返回 None
以表示序列的结束。我们省略了迭代器的许多默认方法,留待后续介绍。
若想用一种自然方式来迭代某个类型,该类型可以实现 std::iter::IntoIterator
,其into_iter
方法接收一个值并返回一个迭代器:
trait IntoIterator where Self::IntoIter: Iterator<Item=Self::Item> {
type Item;
type IntoIter: Iterator;
fn into_iter(self) -> Self::IntoIter;
}
IntoIter
是迭代器值本身的类型,Item
是它产生的值的类型。我们把任何实现了 IntoIterator
的类型称为“可迭代的(iterable)”。
Rust 的 for 循环将所有这些部分很好地结合起来。要迭代一个 vector 中的元素,你可以这样写:
println!("There's:");
let v = vec!["antimony", "arsenic", "aluminum", "selenium"];
for element in &v {
println!("{}", element);
}
for 循环其实只是一种简写,其底层调用了 IntoIterator
和 Iterator
的方法:
let mut iterator = (&v).into_iter();
while let Some(element) = iterator.next() {
println!("{}", element);
}
for 循环使用 IntoIterator::into_iter
将其操作数 &v
转换成一个迭代器,然后重复调用 Iterator::next
。每次返回 Some(element)
后,for 循环就执行其代码主体;如果返回 None
,循环就结束。
思考该例,其中有一些关于迭代器的术语:
- 正如我们所说,迭代器 iterator 是任何实现了
Iterator
的类型。 - 可迭代 iterable 是指任何实现了
IntoIterator
的类型:你可以通过调用它的into_iter
方法得到一个迭代器。本例中 vector 引用&v
是可迭代的。 - 迭代器产生值。
- 迭代器产生的值叫 item。本例中它们是 "antimony"、"arsenic" 等。
- 接收迭代器产生的 item 的代码是 消费者 consumer。即本例中 for 循环的主体代码。
尽管 for 循环总是在其操作数上调用 into_iter
,但其实也可以直接将迭代器传递给 for 循环;例如,当你在 Range 上循环时就属于这种情况。实际上所有的迭代器都自动实现了 IntoIterator
,且其 into_iter
方法会单纯地直接返回迭代器自身:
impl<I: Iterator> const IntoIterator for I {
type Item = I::Item;
type IntoIter = I;
#[inline]
fn into_iter(self) -> I {
self
}
}
如果你在迭代器返回 None
后再次调用 next
方法,Iterator
trait 并没有明确它应该怎么做。大多数迭代器只是再次返回 None
,但并不是全部。(后续在迭代器的适配器 fuse
中详细讲如何处理)。
创建迭代器
Rust 标准库文档详细解释了各类型所提供的迭代器的种类。标准库遵循一些一般惯例,以帮助你获得所需。
iter 和 iter_mut
大多数集合类型提供了 iter
和 iter_mut
方法,它们返回该集合类型的迭代器,迭代器产生对每个 item 的共享引用或可变引用。像数组切片 &[T]
、&mut [T]
也有iter
和 iter_mut
方法。如果你不打算用 for 循环,那么这俩方法是获取迭代器的最常见方法:
let v = vec![4, 20, 12, 8, 6];
let mut iterator = v.iter();
assert_eq!(iterator.next(), Some(&4));
assert_eq!(iterator.next(), Some(&20));
assert_eq!(iterator.next(), Some(&12));
assert_eq!(iterator.next(), Some(&8));
assert_eq!(iterator.next(), Some(&6));
assert_eq!(iterator.next(), None);
该迭代器的 项(item) 的类型是 &i32
:每次调用 next
都会产生对下一个元素的引用,直到到达 vector 的末端。
每种类型都可以依照其目的自由地实现 iter
和 iter_mut
。std::path::Path
的iter
方法返回一个迭代器,每次迭代产生一个 path Component
:
use std::ffi::OsStr;
use std::path::Path;
let path = Path::new("C:/Users/JimB/Downloads/Fedora.iso");
let mut iterator = path.iter();
assert_eq!(iterator.next(), Some(OsStr::new("C:")));
assert_eq!(iterator.next(), Some(OsStr::new("Users")));
assert_eq!(iterator.next(), Some(OsStr::new("JimB")));
...
该迭代器的 item 类型是 &std::ffi::OsStr
,是对 操作系统调用 所接受的字符串切片的借用。
如果某个类型上有不止一种常见的迭代方式,那么该类型通常会为每一种迭代方式提供特定的方法,毕竟单纯地叫作 iter
将会显得模棱两可。例如,在字符串切片类型 &str
上就没有 iter
方法。相应地,如果 s 是 &str
,那么 s.bytes()
会返回一个迭代器,该迭代器产生 s 的每个字节,而 s.chars()
则会将内容解释为 UTF-8 并产生每个 Unicode 字符。
IntoIterator 的实现
当一个类型实现了 IntoIterator
,你便可以调用它的 into_iter
方法,就像 for 循环那样:
// 通常你应该用 HashSet,但是它的迭代顺序是
// 不确定的,所以在例子中 BTreeSet 效果更好。
use std::collections::BTreeSet;
let mut favorites = BTreeSet::new();
favorites.insert("Lucy in the Sky With Diamonds".to_string());
favorites.insert("Liebesträume No. 3".to_string());
let mut it = favorites.into_iter();
assert_eq!(it.next(), Some("Liebesträume No. 3".to_string()));
assert_eq!(it.next(), Some("Lucy in the Sky With Diamonds".to_string()));
assert_eq!(it.next(), None);
大多数集合实际上提供了 IntoIterator
的几种实现,分别用于共享引用(&T)、可变引用(&mut T)和移动(T):
- 给定一个对集合的共享引用,
into_iter
返回一个迭代器,该迭代器产生对其各项的共享引用。例如,在前面的代码中,调用(&favorites).into_iter()
将返回一个迭代器,其项的类型是&String
。 - 给定一个对集合的可变引用,
into_iter
返回一个迭代器,该迭代器产生对其各项的可变引用。例如,假设 vector 是Vec<String>
,调用(&mut vector).into_iter()
将返回一个迭代器,其项的类型是&mut String
。 - 当集合按值传递时,
into_iter
返回一个迭代器,该迭代器取得集合的所有权并按值返回各项;各项的所有权从集合转移到消费者,而原始集合在这个过程中被消耗。例如,在前面的代码中,调用favorites.into_iter()
返回一个迭代器并按值产生各个字符串;消费者获得各个字符串的所有权。当迭代器被 drop 时,BTreeSet 中任何剩余的元素也会被 drop,并且集合本身也被丢弃。
由于 for 循环将 IntoIterator::into_iter
应用于其操作数,上面讲的三种实现对应了以下惯例,分别用于迭代对集合的共享引用、可变引用,或消耗集合并获得其元素的所有权:
for element in &collection { ... }
for element in &mut collection { ... }
for element in collection { ... }
每个 for 都会导致对前文列出的 IntoIterator
实现之一的调用。
当然,并非每个类型都提供所有三种实现。例如,HashSet、BTreeSet 和 BinaryHeap 没有在可变引用上实现 IntoIterator
,因为修改其元素可能会违反该类型的不变性:被修改的值可能有不同的哈希值,或者造成元素乱序。其他类型确实支持 mutation,但也只是部分支持。例如,HashMap 和 BTreeMap 对其条目的值产生了可变引用,但对其键的引用却是共享的,原因与前面给出的类似。
总的原则是,迭代应该有效且可预测。所以 Rust 并没有提供昂贵的或可能表现诡异的实现,而是选择完全省略它们。
切片实现了三个 IntoIterator
变体中的两个;因为它们不拥有自己的元素,所以没有 "按值 "的情况。相应地,针对 &[T]
或 &mut[T]
的 into_iter
方法所返回的迭代器会产生对元素的共享或可变引用。如果你把底层切片类型 [T]
想象成某种集合,这就能很好地融入整个迭代器的运作模式。
你可能已经注意到,前两个 IntoIterator
变体——共享引用和可变引用——相当于在引用上调用 iter
或 iter_mut
。为什么 Rust 同时提供两种方式呢?
IntoIterator
是使 for 循环工作的底层保障,显然是必要的。但是当你不使用 for 循环时,写 favorites.iter()
比写 (&favorites).into_iter()
会更清楚。你经常需要通过共享引用进行迭代,所以 iter
和 iter_mut
仍然有价值,毕竟它们更顺手。
IntoIterator
在泛型代码中也很有用:可以使用 T: IntoIterator
来限制类型变量 T,使其成为可以被迭代的类型。或者,可以用 T: IntoIterator<Item=U>
来进一步要求迭代产生特定的类型 U。例如下面的函数,它从任意可迭代的类型中转储其值,且其迭代器的项可以用 "{:?}"
格式打印:
use std::fmt::Debug;
fn dump<T, U>(t: T)
where T: IntoIterator<Item=U>,
U: Debug
{
for u in t {
println!("{:?}", u);
}
}
你不能用 iter
和 iter_mut
来写这个泛型函数,因为它们不是任何 trait 的方法。只是大多数可迭代类型恰好包含这两个名字的方法而已。
from_fn 和 successor
产生序列值的另一个简单而通用的方法是提供一个返回它们的闭包。
给定一个返回 Option<T>
的函数 F,std::iter::from_fn
可以返回一个迭代器,该迭代器直接调用 F 函数来产生值。比如:
use rand::random;
use std::iter::from_fn;
// 生成 1000 条随机线段,这些线段的端点均匀地分布在区间[0, 1]内。
let lengths: Vec<f64> =
from_fn(|| Some((random::<f64>() - random::<f64>()).abs()))
.take(1000)
.collect();
例中调用 from_fn
来生成一个产生随机数的迭代器。由于迭代器总是返回 Some
,所以序列永不结束,但我们通过调用 take(1000)
来限制它返回前 1000 个元素。然后用 collect
从迭代结果中建立 vector。这也是一种初始化 vector 的有效方法。
若是各个项依赖于前一个项,std::iter::successors
函数可以很好地胜任此工作。你需要提供一个初始项和一个函数,该函数接收一个项并返回下一个项的 Option
。如果它返回 None
,迭代就结束了。例如:
use num::Complex;
use std::iter::successors;
fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> {
let zero = Complex { re: 0.0, im: 0.0 };
successors(Some(zero), |&z| { Some(z * z + c) })
.take(limit)
.enumerate()
.find(|(_i, z)| z.norm_sqr() > 4.0) .map(|(i, _z)| i)
}
从 zero 开始,通过重复对最后一个点进行平方,并加上参数 c,调用 successor 在复平面上产生一个点序列。
from_fn
和 successors
都接受 FnMut
闭包,所以它可以从周围的作用域中捕获并修改变量。例如,下面这个 fibonacci
函数使用 move 闭包来捕获变量并将其作为运行状态:
fn fibonacci() -> impl Iterator<Item=usize> {
let mut state = (0, 1);
std::iter::from_fn(move || {
state = (state.1, state.0 + state.1);
Some(state.0)
})
}
assert_eq!(fibonacci().take(8).collect::<Vec<_>>(),
vec![1, 1, 2, 3, 5, 8, 13, 21]);
需要注意的是:from_fn
和 successor
方法足够灵活,致使你可以通过复杂的闭包,把对迭代器的任何调用变成对一个元素的单一调用。但这样做却忽视了迭代器的价值——即明确数据如何在计算中流转,并为常见模式使用标准命名。在使用这两个迭代器之前,请确保你已经熟悉了其他迭代器方法,因为通常有更好的方法来完成工作。
drain 方法
许多集合类型提供了 drain
方法,它接收一个对集合的可变引用,并返回一个迭代器,该迭代器将每个元素的所有权传递给消费者。然而,与 into_iter()
方法“按值”消费集合所不同的是,drain
只是借用了一个对集合的可变引用,当迭代器被 drop 时,它会从集合中删除所有剩余元素,使其为空。
一些可以通过范围进行索引的类型(比如 String、vector 和 VecDeque),drain
方法会移除一定范围的元素,而不是整个序列:
use std::iter::FromIterator;
let mut outer = "Earth".to_string();
let inner = String::from_iter(outer.drain(1..4));
assert_eq!(outer, "Eh");
assert_eq!(inner, "art");
如果你确实需要移除整个序列,请使用全部范围 ...
作为参数。
迭代器的适配器
迭代器的 Iterator
trait 提供了大量的适配器方法,或者简单说成适配器,它们可以消费一个迭代器并建立另一个新迭代器,新的迭代器具有一些实用功能。为了了解适配器如何工作,我们将从两个最流行的适配器开始——即 map
和 filter
。然后继续介绍适配器工具箱的其余部分,包括几乎所有你能想到的从其他序列中生成数值序列的方法:截断、跳过、组合、反转、连接、重复等等。
map 和 filter
Iterator
trait 中的 map
适配器通过使用闭包来对迭代器的各项元素进行转换。filter
适配器则可从迭代器中过滤出需要的项,它同样是使用一个闭包来决定哪些项要保留,哪些项要丢弃。
例如,假设你正在迭代文本行,并想去除每一行文本的前后空白。标准库的 str::trim
方法可以将前导和尾部的空白从单一的 &str
中删除并返回一个新的、裁剪过的 &str
,它借用了原来的 &str
。这时,可以使用 map
适配器将 str::trim
应用于迭代器中的每一行:
let text = " ponies \n giraffes\niguanas \nsquid".to_string();
let v: Vec<&str> = text.lines()
.map(str::trim)
.collect();
assert_eq!(v, ["ponies", "giraffes", "iguanas", "squid"]);
调用 text.lines()
返回一个迭代器,产生字符串的各行。在该迭代器上调用 map
会返回第二个迭代器,这个迭代器将 str::trim
应用于字符串各行,并将 trim 的结果作为项。最后,collect
将这些项收集到 vector 中。
当然,map
返回的迭代器本身也可以被进一步适配。如果你想把 iguanas 从结果中排除,可以:
let text = " ponies \n giraffes\niguanas \nsquid".to_string();
let v: Vec<&str> = text.lines()
.map(str::trim)
.filter(|s| *s != "iguanas")
.collect();
assert_eq!(v, ["ponies", "giraffes", "squid"]);
此处,filter
返回了第三个迭代器,它在 map
迭代器之后继续使用闭包 |s| *s != "iguanas"
,filter
迭代器产生的是闭包返回 true
的各项。一条迭代器适配器链就像 Unix shell 中的 pipeline:其中每个适配器都有特定目标,当我们从左到右阅读代码时,我们很清楚整个序列是如何被转换的。
这俩适配器签名如下:
fn map<B, F>(self, f: F) -> impl Iterator<Item=B>
where Self: Sized, F: FnMut(Self::Item) -> B;
fn filter<P>(self, predicate: P) -> impl Iterator<Item=Self::Item>
where Self: Sized, P: FnMut(&Self::Item) -> bool;
在标准库中,map
和 filter
实际上返回了特定的不透明数据类型(opaque struct),分别为 std::iter::Map
和 std::iter::Filter
。然而仅看它们的名字并不能提供什么有用信息,所以我们写作 -> impl Iterator<Item=...>
,因为这才表示出真正返回的东西:一个产生给定类型 Item 的 Iterator
。
由于大多数适配器都是通过值来获取 self 的,因此要求 Self
必须是 Sized
(所有常见迭代器都是 Sized
)。
map
迭代器通过值将各项传递给它的闭包,并反过来将闭包结果的所有权传递给它的消费者。filter
迭代器通过共享引用将各项传递给它的闭包,并在项被选中传递给消费者时保留所有权。这就是为什么示例中必须 解引用 s(*s
) 才能将其与 "iguanas"
进行比较:filter
迭代器各项的类型是 &str
,因此闭包参数 s 的类型是 &&str
。
关于迭代器适配器,有两个要点需要注意。
首先,在迭代器上调用适配器并不会消耗任何 item。适配器仅是返回一个新的迭代器,并为从第一个迭代器中提取 item 做好准备。在一连串的适配器中,只有在最后一个迭代器上调用 next
才会真正开始迭代工作。
因此在前面的例子中,调用 text.lines()
方法本身实际上并不解析字符串中任何行。该方法只是返回了一个迭代器,只有在需要时才会开始解析各行。同样,map
和 filter
也只是返回一个新的迭代器,只有在需要时才会进行映射或过滤。在 collect
开始调用 filter
迭代器的 next
之前,不会有任何迭代工作发生。
如果所使用的适配器有副作用,这一点尤为重要。例如,这段代码根本不会打印任何内容:
["earth", "water", "air", "fire"]
.iter().map(|elt| println!("{}", elt));
调用 iter
返回数组元素的迭代器,再调用 map
返回第二个迭代器,它将闭包应用于第一个迭代器所产生的每个值。但是,此时并没有从整个调用链中实际要求一个值,没有运行 next
方法。事实上,Rust 会对此给出警告:
警告信息中的 "lazy" 一词并非贬义。它是一种行话,指任何将计算推迟到需要其值时才进行的机制。按照 Rust 的惯例,迭代器应该在每次调用 next
时做最少的运算,本例中根本没有这样的调用,所以没有运算。
第二个要点是,迭代器适配器是一种零成本抽象。 map
、filter
和他们的其它小伙伴都是非常常用的方法,因此将它们应用于迭代器时,会针对所涉及的特定迭代器类型对代码进行特殊处理。这意味着 Rust 有足够的信息将每个迭代器的 next
方法内联到消费者中,然后将整个编排作为一个单元翻译成机器代码。因此之前展示的 lines/map/filter
迭代器链就像手工编写的代码一样高效:(译者注:意思是迭代器的链式调用,并不会像执行多次 for 循环那样,而更类似下面的代码)
for line in text.lines() {
let line = line.trim();
if line != "iguanas" {
v.push(line);
}
}
filter_map 和 flat_map
如果适配器的每个输入项都只产生一个输出项,使用 map
是没问题的。但是,如果想从迭代中删除某些项,或者用零或多个其它项来替换某项,该怎么办呢?filter_map
和 flat_map
适配器应运而生。
filter_map
适配器与 map
类似,只是它除了可以通过闭包函数将某项转换为新项(类同 map
的功能)外,还可以将项从迭代中删除。因此,它有点像 filter
和 map
的结合体。其签名如下:
fn filter_map<B, F>(self, f: F) -> impl Iterator<Item=B>
where Self: Sized, F: FnMut(Self::Item) -> Option<B>;
与 map
的签名基本相同,只是此处闭包返回的是 Option<B>
类型,而不是简单的 B
。当闭包返回 None
时,迭代中的项将被删除;而当闭包返回 Some(b)
时,b
便是 filter_map
产生的下一项。
例如,假设要扫描一个字符串,查找用空白分隔的、可以解析为数字的单词,然后处理其中的数字,去掉其他词。可以这样写:
use std::str::FromStr;
let text = "1\nfrond .25 289\n3.1415 estuary\n";
for number in text
.split_whitespace()
.filter_map(|w| f64::from_str(w).ok()) {
println!("{:4.2}", number.sqrt());
}
示例打印结果:
1.00
0.50
17.00
1.77
(译者注:该示例打印了 number.sqrt()
的结果,反而让例子看起来很别扭。number
的输出结果为:1.00
、0.25
、289.00
、3.14
)
传入 filter_map
的闭包尝试用 f64::from_str
解析每个以空白分隔的单词,然后返回 一个 Result<f64,ParseFloatError>
,.ok()
再将其转化为一个 Option<f64>
:解析失败的结果为 None
,而解析成功的结果为 Some(v)
。filter_map
迭代器会删除所有 None
值,并为每个 Some(v)
生成值 v
。
但是,像这样把 map
和 filter
融合到一个操作中,而不是直接使用这些适配器,又有什么意义呢?filter_map
适配程序在类似刚才的情况下显示出了其价值,在这种情况下,决定是否将某个项包含在迭代中的最佳方法就是尝试实际去处理它。只用 filter
和 map
也能做同样的事情,但有点不够优雅:
text.split_whitespace()
.map(|w| f64::from_str(w))
.filter(|r| r.is_ok())
.map(|r| r.unwrap())
flat_map
与 map
、filter_map
一脉相承,只不过它的闭包返回的不像 map
那样只是一个项,也不像 filter_map
那样是零或一个项,而是由任意多个项组成的序列。flat_map
迭代器则将闭包返回的这些项串联起来后返回。
其签名如下:
fn flat_map<U, F>(self, f: F) -> impl Iterator<Item=U::Item>
where F: FnMut(Self::Item) -> U, U: IntoIterator;
传递给 flat_map
的闭包必须返回一个可迭代类型(实现了 IntoIterator
的类型),任何可迭代类型都可以。
假如我们有一个将国家映射到其主要城市的表格。给定一个国家列表后,该如何遍历这些国家的主要城市呢?
use std::collections::HashMap;
let mut major_cities = HashMap::new();
major_cities.insert("Japan", vec!["Tokyo", "Kyoto"]);
major_cities.insert("The United States", vec!["Portland", "Nashville"]);
major_cities.insert("Brazil", vec!["São Paulo", "Brasília"]);
major_cities.insert("Kenya", vec!["Nairobi", "Mombasa"]);
major_cities.insert("The Netherlands", vec!["Amsterdam", "Utrecht"]);
let countries = ["Japan", "Brazil", "Kenya"];
for &city in countries.iter().flat_map(|country| &major_cities[country]) {
println!("{}", city);
}
代码输出结果:
Tokyo
Kyoto
São Paulo
Brasília
Nairobi
Mombasa
可以这样理解:针对每个国家检索其城市的 vector,将所有 vector 连接成一个序列,然后打印出来。
但请记住,迭代器是惰性的:只有 for 循环调用 flat_map
迭代器的 next
方法才会导致工作的完成,内存中永远不会构建完整的连接序列。取而代之的是一个小的状态机,它从城市迭代器中每次抽取一个项,直到用完为止,然后才为下一个国家生成新的城市迭代器。其效果就像嵌套循环,只不过被包装成了一个迭代器。
flatten
flatten
适配器会连接一个迭代器的各项,并假设每个项本身都是可迭代的:
use std::collections::BTreeMap;
// 城市与公园的映射表:每个值都是个 vector
let mut parks = BTreeMap::new();
parks.insert("Portland", vec!["Mt. Tabor Park", "Forest Park"]);
parks.insert("Kyoto", vec!["Tadasu-no-Mori Forest", "Maruyama Koen"]);
parks.insert("Nashville", vec!["Percy Warner Park", "Dragon Park"]);
// 建立一个包含所有公园的 vector。`values` 返回一个产生
// vector 的迭代器,然后 `flatten` 依次返回每个 vector 的元素。
let all_parks: Vec<_> = parks.values().flatten().cloned().collect();
assert_eq!(all_parks,
vec!["Tadasu-no-Mori Forest", "Maruyama Koen", "Percy Warner Park",
"Dragon Park", "Mt. Tabor Park", "Forest Park"]);
"flatten" 这个名称的意思是将两层结构扁平化为一层结构:BTreeMap
中所有放置公园名称的 vec
被扁平化为能产生所有名称的迭代器。
flatten
的签名如下:
fn flatten(self) -> impl Iterator<Item=Self::Item::Item>
where Self::Item: IntoIterator;
换句话说,底层迭代器的项本身必须实现 IntoIterator
,它实际上就是可迭代的。然后,flatten
方法会串起各个项并返回串联后各项的迭代器。当然,这个过程是惰性的,只有迭代完上一个迭代项时,才会从 self
中得出一个新的迭代项。
flatten
方法有几种出人意料的用法。假设有个 Vec<Option<...>>
,现在想遍历 其中的 Some
值,那么 flatten
方法就能很好地搞定这个问题:
assert_eq!(vec![None, Some("day"), None, Some("one")]
.into_iter()
.flatten()
.collect::<Vec<_>>(),
vec!["day", "one"]);
这是因为 Option
本身实现了 IntoIterator
,它代表着一个由 0 或 1 个元素组成的序列。None
元素为迭代贡献了 0 个值,而每个 Some
元素都贡献 1 个值。同样,你也可以使用 flatten
对 Option<Vec<...>>
的值进行迭代: None
的行为与空 vector 相同。
Result
也实现了 IntoIterator
,其中 Err
代表空序列。因此对返回 Result
值的迭代器使用 flatten
的话,可以有效排除其中所有 Err
,从而得到一个未封装的只含成功值的流。我们并不建议在代码中忽略错误,但在足够了解情况时这确实是一种巧妙技巧。
你可能会发现,当真正需要的是 flat_map
时,你却在使用 flatten
。例如,标准库中的 str::to_uppercase
方法可以将字符串转换为大写字母,它的工作原理是这样的:
fn to_uppercase(&self) -> String {
self.chars()
.map(char::to_uppercase)
.flatten() // 此处有更好的方法
.collect()
}
之所以需要 flatten
是因为 ch.to_uppercase()
返回的不是单个字符,而是返回了能够产生一个或多个字符的迭代器。将每个字符映射到大写的过程会产生一个由字符迭代器组成的迭代器,而 flatten
会将这些字符迭代器拼接起来,最终 collect
为 String
。
这种 map
和 flaten
的组合非常常见,因此 Iterator
提供了 flat_map
适配器来处理这种情况(其实 flat_map
比 flatten
更早加入标准库):
fn to_uppercase(&self) -> String {
self.chars()
.flat_map(char::to_uppercase)
.collect()
}
take 和 take_while
Iterator
trait 中的 take
和 take_while
适配器可以在迭代了一定数量的项之后结束迭代,或是根据闭包来决定何时结束迭代。它们的签名如下:
fn take(self, n: usize) -> impl Iterator<Item=Self::Item>
where Self: Sized;
fn take_while<P>(self, predicate: P) -> impl terator<Item=Self::Item>
where Self: Sized, P: FnMut(&Self::Item) -> bool;
这两种迭代器都会获取迭代器的所有权,并返回一个新的迭代器,该迭代器从头开始返回各项,但可能会提前结束迭代。take
迭代器最多产生 n 个项,之后则返回 None
。take_while
迭代器会对每个项执行 predicate
,并在 predicate
第一次返回 false
时返回 None
,之后每次调用 next
也会返回 None
。
例如,在给定的电子邮件 message 中,邮件头 header 和正文之间有空行隔开,可以使用 take_while
只遍历邮件头:
let message = "To: jimb\r\n\
From: superego <editor@oreilly.com>\r\n\
\r\n\
Did you get any writing done today?\r\n\
When will you stop wasting time plotting fractals?\r\n";
for header in message.lines()
.take_while(|l| !l.is_empty()) {
println!("{}" , header);
}
Rust 的字符串字面量中,当字符串中的一行以反斜线结束时,不会包含字符串下一行的缩进。因此字符串中的任一行都没有前导空白。这意味着 message 的第三行是空白,take_while
适配器一看到这行空白就会终止迭代,继而这段代码只打印了前两行:
To: jimb
From: superego <editor@oreilly.com>
skip 和 skip_while
Iterator
trait 的 skip
和 skip_while
方法是对 take
和 take_while
方法的补充:它们会从迭代开始丢弃指定数量的项,或是根据闭包来决定丢弃多少项,然后原封不动地返回剩余项。签名如下:
fn skip(self, n: usize) -> impl Iterator<Item=Self::Item>
where Self: Sized;
fn skip_while<P>(self, predicate: P) -> impl Iterator<Item=Self::Item>
where Self: Sized, P: FnMut(&Self::Item) -> bool;
skip
适配器的一个常见用途是在命令行程序中,跳过命令名称然后遍历其后的参数。
for arg in std::env::args().skip(1) {
...
}
std::env::args
函数返回一个迭代器,迭代器以字符串形式产生命令行程序的参数,其中第一个参数是程序本身的名称。它不是我们打算在循环中处理的字符串。而在该迭代器上调用 skip(1)
会返回另一个新的迭代器,新的迭代器被调用时会丢弃程序名,然后返回所有后续参数。
skip_while
适配器使用闭包来决定丢弃多少个项。可以像这样遍历前一篇中的消息正文行:
for body in message.lines()
.skip_while(|l| !l.is_empty())
.skip(1) {
println!("{}" , body);
}
使用 skip_while
跳过非空行,但迭代器本身也会产生空行——毕竟闭包在空行时返回了 false
。因此,我们再使用 skip
方法跳过该行,从而得到一个迭代器,其第一项将是消息正文的第一行。参见前一篇中的 message 声明,这段代码将打印出:
Did you get any writing done today?
When will you stop wasting time plotting fractals?
fuse
一旦迭代器返回了 None
,如果再次调用其 next
方法的话,trait 并没有约定如何处理这种情况。尽管大多数迭代器都会再次返回 None
,但并非所有。而如果代码依赖此情况,则可能产生怪异效果。
fuse
适配器可以接受任意迭代器,并生成一个一旦第一次返回 None
,就一定会继续返回 None
的迭代器:
struct Flaky(bool);
impl Iterator for Flaky {
type Item = &'static str;
fn next(&mut self) -> Option<Self::Item> {
if self.0 {
self.0 = false;
Some("最后一项")
} else {
self.0 = true;
None
}
}
}
let mut flaky = Flaky(true);
assert_eq!(flaky.next(), Some("最后一项"));
assert_eq!(flaky.next(), None);
assert_eq!(flaky.next(), Some("最后一项"));
let mut not_flaky = Flaky(true).fuse();
assert_eq!(not_flaky.next(), Some("最后一项"));
assert_eq!(not_flaky.next(), None);
assert_eq!(not_flaky.next(), None);
在需要处理来历不明的迭代器的通用代码中,fuse
适配器非常有用。与其寄希望于待处理的每个迭代器都遵守约定,不如使用 fuse
来确保这一点。
peekable
peekable 迭代器可以在不产生实际消耗的情况下检查下一个项。调用 Iterator
trait 的 peekable
方法即可:
fn peekable(self) -> std::iter::Peekable<Self>
where Self: Sized;
此处 Peekable<Self>
是一个实现了 Iterator<Item=Self::Item>
的结构体,而 Self
是底层迭代器的类型。
Peekable
迭代器有个额外的方法 peek
,该方法返回 Option<&Item>
:如果底层迭代器已完成迭代,则返回 None
,否则返回 Some(r)
,其中 r
是指向下一个项的共享引用。(注意,如果迭代器返回项的类型已经是对某项的引用,那么最终返回的将是对某个引用的引用)。
调用 peek
会尝试从底层迭代器中抽取下一个项,若存在,则将它缓存起来直到下次调用 next
。Peekable
上的其他所有 Iterator
相关方法都知道该缓存的存在:例如 iter.last()
的 iter
就知道在耗尽底层迭代器后检查缓存。
当如果不提前窥探就无法决定从一个迭代器中消耗多少项时,peekable 迭代器就显得非常重要。例如,如果要从字符流中解析数字,在数字后面的第一个非数字字符出现之前,是无法确定数字在哪里结束的:
use std::iter::Peekable;
fn parse_number<I>(tokens: &mut Peekable<I>) -> u32
where I: Iterator<Item=char>
{
let mut n = 0;
loop {
match tokens.peek() {
Some(r) if r.is_digit(10) => {
n = n * 10 + r.to_digit(10).unwrap();
}
_ => return n
}
tokens.next();
}
}
let mut chars = "226153980,1766319049".chars().peekable();
assert_eq!(parse_number(&mut chars), 226153980);
// `parse_number` 还没有消费逗号! 因此可以:
assert_eq!(chars.next(), Some(','));
assert_eq!(parse_number(&mut chars), 1766319049);
assert_eq!(chars.next(), None);
函数 parse_number
使用 peek
来检查下一个字符,只有当它是数字时才实际消耗它。如果不是数字或迭代器耗尽(即 peek
返回 None
),将会返回已解析的数字,并把下一个字符留在迭代器中以备使用。
可逆迭代器和 rev 方法
有些迭代器可以从序列的两端提取项目。使用 rev
适配器可以逆转此类迭代器。例如,vector 上的迭代器可以很容易地从末端提取项,就像从起点提取项一样容易。这类迭代器实现了 std::iter::DoubleEndedIterator
trait,该 trait 扩展了 Iterator
:
trait DoubleEndedIterator: Iterator {
fn next_back(&mut self) -> Option<Self::Item>;
}
可以把双端迭代器想象成两根手指,分别标记序列的前端和后端。从任意一端提取项都会使该手指向另一端移动;当两根手指相遇时,迭代结束:
let bee_parts = ["头", "胸", "腹"];
let mut iter = bee_parts.iter();
assert_eq!(iter.next(), Some(&"头"));
assert_eq!(iter.next_back(), Some(&"腹"));
assert_eq!(iter.next(), Some(&"胸"));
assert_eq!(iter.next_back(), None);
assert_eq!(iter.next(), None);
切片上迭代器的结构使得该行为很容易实现:实际上它是一对指针,分别指向尚未产生的元素的起点和终点;next
和 next_back
只需从其中之一提取一个项即可。有序集合(如 BTreeSet
和 BTreeMap
)的迭代器也是双端的:它们的 next_back
方法会首先提取最大的元素。一般来说,在可行的情况下标准库都会提供双端迭代。
然而并不是所有的迭代器都能轻易实现该行为:一个从其他线程产生值的迭代器到达通道的 Receiver
时,无法预知最后接收到的值是什么。一般来说,你需要查看标准库的文档,以了解哪些迭代器实现了 DoubleEndedIterator
,哪些没有。
如果迭代器是双端的,可以使用 rev
适配器将其反转:
fn rev(self) -> impl Iterator<Item=Self>
where Self: Sized + DoubleEndedIterator;
rev
返回的迭代器也是双端的,其实它只是把 next
和 next_back
方法进行了简单交换:
let meals = ["早餐", "午餐", "晚餐"];
let mut iter = meals.iter().rev();
assert_eq!(iter.next(), Some(&"晚餐"));
assert_eq!(iter.next(), Some(&"午餐"));
assert_eq!(iter.next(), Some(&"早餐"));
assert_eq!(iter.next(), None);
对于大多数迭代器适配器,如果应用于可逆迭代器,则会返回另一个可逆迭代器。例如,map 和 filter 就保留了可逆性。
(译注:参考下例)
let meals = ["breakfast", "lunch", "dinner"];
let mut iter = meals.iter()
.map(|w| w.chars().next().unwrap())
.rev();
assert_eq!(iter.next(), Some('d'));
inspect
inspect
适配器在调试迭代器适配器 pipeline 时非常方便,但在生产代码中并不常用。它只是简单地对每个项应用一个闭包,并将项传递下去。闭包不能影响项本身,但可以执行诸如打印或断言等操作。
该例展示了将字符串转换为大写时会改变其长度的情况:
let upper_case: String = "große".chars()
.inspect(|c| println!("转换前: {:?}", c))
.flat_map(|c| c.to_uppercase())
.inspect(|c| println!("转换后: {:?}", c))
.collect();
assert_eq!(upper_case, "GROSSE");
德语小写字母 "ß" 的大写等效字母是 "SS",这也正是为什么 char::to_uppercase
会返回一个字符迭代器,而不是单个的替换字符。上面的代码使用 flat_map
将 to_uppercase
返回的所有项连接成一个单独的字符串,并在此过程中打印如下内容:
转换前: 'g'
转换后: 'G'
转换前: 'r'
转换后: 'R'
转换前: 'o'
转换后: 'O'
转换前: 'ß'
转换后: 'S'
转换后: 'S'
转换前: 'e'
转换后: 'E'
chain
chain
适配器将一个迭代器附加到另一个迭代器的末尾。具体来说就是,i1.chain(i2)
返回一个迭代器,该迭代器先从 i1
中获取各项,直到 i1
被耗尽后再从 i2
中获取项。
chain
适配器的签名如下:
fn chain<U>(self, other: U) -> impl Iterator<Item=Self::Item>
where Self: Sized, U: IntoIterator<Item=Self::Item>;
换句话说,有了该适配器后,可以将一个迭代器与任何产生相同类型的可迭代对象进行链式连接。
比如:
let v: Vec<i32> = (1..4).chain(vec![20, 30, 40])
.collect();
assert_eq!(v, [1, 2, 3, 20, 30, 40]);
如果链式迭代器的两个底层迭代器均可逆,那么链式迭代器本身也可逆:
let v: Vec<i32> = (1..4).chain(vec![20, 30, 40])
.rev()
.collect();
assert_eq!(v, [40, 30, 20, 3, 2, 1]);
链式迭代器会跟踪两个底层迭代器是否返回了 None
,并根据需要在其中一个迭代器上调用 next
或 next_back
方法。
enumerate
Iterator
trait 中的 enumerate
适配器可以将递增的索引附加到一个序列上。它若接受一个产生元素 A、B、C... 的迭代器,则会返回一个产生元组 (0, A)、(1, B)、(2, C)... 的迭代器。乍一看似乎很简单,但其实该适配器还挺实用的。
可以将 enumerate
生成的这种 (index, item)
和 HashMap 的 (key, value)
作对比。如果在切片或 vector 上进行迭代,索引就是各项中的 “key”。
let v = vec![2, 4, 6, 7, 8];
for (i, num) in v.into_iter().enumerate() {
println!("索引: {},值:{}", i, num);
}
输出:
索引: 0,值:2
索引: 1,值:4
索引: 2,值:6
索引: 3,值:7
索引: 4,值:8
zip
zip
适配器将两个迭代器合并为一个,该迭代器产生从各个迭代器中取出的值对,就像拉链将两侧合并成一条线缝一样。当其中任一底层迭代器结束时,被合并后的迭代器都将结束。
例如,通过将无穷 Range 0..
与另一个迭代器进行合并后,就可以得到和 enumerate
相同的效果:
let v: Vec<_> = (0..).zip("ABCD".chars()).collect();
assert_eq!(v, vec![(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D')]);
在这一意义上,可以将 zip
看作是 enumerate
的一种泛化:enumerate
将索引附加到序列上,而 zip
将任意迭代器的项进行合并。之前我们提到,enumerate
可以为待处理项提供上下文,而 zip
则是以更灵活的方式实现相同的功能。
zip
函数的参数不一定必须是迭代器本身,还可以是任何可迭代对象:
use std::iter::repeat;
let endings = vec!["once", "twice", "chicken soup with rice"];
let rhyme: Vec<_> = repeat("going")
.zip(endings)
.collect();
assert_eq!(rhyme, vec![("going", "once"),
("going", "twice"),
("going", "chicken soup with rice")]);
by_ref 方法
一旦在迭代器上附加了适配器,是否可以再次将适配器移除呢?通常来说,不可以!适配器会持有底层迭代器的所有权,并且不提供将其返还的方法。
迭代器的 by_ref
方法借用了迭代器的可变引用,这样一来,便可以将适配器应用于该引用。当消费完适配器中的项后,借用结束,并重新获得对原始迭代器的访问权限。
之前的文章中使用 take_while
和 skip_while
处理过邮件消息的头部和正文。但若希望使用相同的底层迭代器同时处理两者怎么办呢?可以通过 by_ref
,首先使用 take_while
处理头部,当完成后,获取回底层迭代器,这时 take_while
将恰好在处理消息正文的位置上:
let message = "To: jimb\r\n\
From: id\r\n\
\r\n\
Oooooh, donuts!!\r\n";
let mut lines = message.lines();
println!("头部:");
for header in lines.by_ref().take_while(|l| !l.is_empty()) {
println!("{}", header);
}
println!("\n正文:");
for body in lines {
println!("{}", body);
}
调用 lines.by_ref()
借用了迭代器的可变引用,而 take_while
迭代器则拥有该引用的所有权。该迭代器在第一个 for
循环结束时超出作用域,意味着借用已经结束,因此可以在第二个 for
循环中再次使用 lines
。上例输出:
头部:
To: jimb
From: id
正文:
Oooooh, donuts!!
by_ref
适配器的定义非常简单:它返回迭代器的可变引用。然后,标准库中给了个古灵精怪的实现:
impl<'a, I: Iterator + ?Sized> Iterator for &'a mut I {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
(**self).next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(**self).size_hint()
}
}
换句话说,如果 I
是某种迭代器类型,那么 &mut I
也是一个迭代器,其 next
和 size_hint
方法都由其引用者来处理。对迭代器的可变引用调用适配器时,适配器获取的是引用的所有权,而不是迭代器本身。它只是一个在适配器超出作用域时便会结束的借用。
cloned 和 copied
cloned
适配器接收一个生成引用的迭代器,并返回一个能够生成从这些引用克隆而来的值的迭代器,功能类似于 iter.map(|item| item.clone())
。显然,被引用的类型必须实现 Clone
trait。例如:
let a = ['1', '2', '3', '∞'];
assert_eq!(a.iter().next(), Some(&'1'));
assert_eq!(a.iter().cloned().next(), Some('1'));
copied
适配器与 cloned
适配器思路相通,只是更加严格:被引用的类型必须实现 Copy
trait。形如 iter.copied()
的调用与 iter.map(|r| *r)
大致相同。由于每个实现 Copy
trait 的类型也都实现了 Clone
trait,因此 cloned
更加通用些。但是,根据项的类型的不同,clone
调用可能会进行任意数量的内存分配及复制操作。如若假设此情况永远不会发生——因为项的类型很简单——则最好使用 copied
,以便类型检查器进行检查。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。