【2025 Rust学习 --- 14 迭代器创建和使用】
迭代器
迭代器是一个值,它可以生成一系列值,通常用来执行循环操作。
Rust 的标准 库不仅提供了用于遍历向量、字符串、哈希表和其他集合的迭代器,还提供了 “从输入流中产生文本行”“从网络服务器中产生新的入站连接”“从通信通道中其他线程接收的值”等迭代器。当然,你也可以出于自己的目的实现迭代器。
Rust 的 for 循环为使用迭代器提供了一种自然的语法,但迭代器本身也提供了一组丰富 的方法,比如映射(map)、过滤(filter)、连接(join)、收集 (collect)等。
fn triangle(n: i32) -> i32 {
let mut sum = 0;
for i in 1..=n {
sum += i;
}
sum
}
表达式1..=n
是一个 RangeInclusive<i32>
型的值。RangeInclusive<i32>
是一个迭代器,可以生成其起始值到结束值(包括两 者)之间的整数,因此你可以将它用作 for 循环的操作数来对从 1 到 n 的值求 和。 但是迭代器还有一个 fold
方法,可以实现完全一样的效果:
fn triangle(n: i32) -> i32 {
(1..=n).fold(0, |sum, item| sum + item)
}
开始运行时以 0 作为总和,fold 会获取 1…=n 生成的每个值,并以总和 (sum)跟当前值(item)为参数调用闭包 |sum, item| sum + item。闭包的返回值会作为新的总和
发行版中,Rust 会理解 fold 的定义并将其内联到 triangle 中。接下来是将闭包|sum, item| sum + item
内联到 triangle 中。最 后,Rust 会检查合并后的代码并意识到有一种更简单的方法可以对从 1 到 n 的 数值求和:其总和总会等于 n * (n+1) / 2
。于是 Rust 将 triangle 的整个 函数体,包括循环、闭包和所有内容,翻译成了单个乘法指令和几个算术运算。
特型:Iterator与IntoIterator
迭代器是实现了 std::iter::Iterator 特型的任意值
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
…… // 很多默认方法
}
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 的类型都称为可迭代者,因为你可以随意迭代它。
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
的任意类型。 - 可迭代者是任何实现了
IntoIterator
的类型:你可以通过调用它的 into_iter 方法来获得一个迭代器。在这里,向量引用 &v 就是可迭代 者。 - 迭代器能生成值。
- 迭代器生成的值是条目。
- 接收迭代器所生成条目的代码是消费者。在这里,for 循环体就是消费者。
虽然 for 循环总会在其操作数上调用 into_iter,但也可以直接把迭代器传给 for 循环,比如,在遍历 Range 时就是这样的。所有迭代器都自动实现了 IntoIterator
,并带有一个直接返回迭代器的 into_iter 方法。
如果在返回 None 后再次调用迭代器的 next 方法,则 Iterator 特型没有规 定它应该做什么。大多数迭代器只会再次返回 None,但也有例外。
创建迭代器
iter 方法与 iter_mut 方法
大多数集合类型提供了 iter(迭代器)方法和 iter_mut(可变迭代器)方 法,它们会返回该类型的自然迭代器,为每个条目生成共享引用或可变引用。
像 &[T] 和 &mut [T] 这样的数组切片也有 iter 方法和 iter_mut 方法。如果 你不打算让 for 循环替你跟迭代器打交道,iter 方法和 iter_mut 方法就是 获取迭代器最常用的方法:
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);
这个迭代器的条目类型是 &i32:每次调用 next 都会生成对下一个元素的引 用,直到抵达向量的末尾。
每种类型都可以多态地实现 iter 和 iter_mut。
如果类型有不止一种常用的遍历方式,该类型通常会为每种遍历方式提供一个专门的方法,因为普通的 iter 方法会产生歧义。例如,&str 字符串切片类型就 没有 iter 方法——如果 s 是 &str:
- 则 s.bytes() 会返回一个能生成 s 中每 字节的迭代器
- 而 s.chars() 则会将内容解释为 UTF-8 并生成每个 Unicode 字符。
IntoIterator 的实现
use std::collections::BTreeSet;
let mut tree01 = BTreeSet::new();
favorites.insert("Lucy".to_string());
favorites.insert("Lily".to_string());
let mut it = tree01.into_iter();
assert_eq!(it.next(), Some("Jenny".to_string()));
assert_eq!(it.next(), Some("Diamonds".to_string()));
assert_eq!(it.next(), None);
大多数集合实际上提供了 IntoIterator 的几种实现,用于共享引用(&T)、 可变引用(&mut T)和移动(T)
- 给定一个集合的共享引用,into_iter 会返回一个迭代器,该迭代器会生成对其条目的共享引用。(&favorites).into_iter() 会返回一个 Item 类型为 &String 的迭代器。
- 给定对集合的可变引用,into_iter 会返回一个迭代器,该迭代器会生成对其条目的可变引用。如果 vector 是某个 Vec,则调用 (&mut vector).into_iter() 会返回一个 Item 类型为 &mut String 的迭代器。
- 当按值传递集合时,into_iter 会返回一个迭代器,该迭代器会获取集合的所有权并按值返回这些条目,这些条目的所有权会从集合转移给消费者, 原始集合在此过程中已被消耗掉了。tree01.into_iter() 调用返回了一个迭代器,该迭代器会按值生成每个字符串,消费者会获得每个字符串的所有权。当迭代器被丢弃时, BTreeSet 中剩余的所有元素都将被丢弃,并且该集合的空壳tree01也将被丢弃。
由于 for 循环会将IntoIterator::into_iter
作为它的操作对象,因此这 3 种实现创建了以下惯用法,用于迭代对集合的共享引用或可变引用,或者消耗 该集合并获取其元素的所有权:
for element in &collection { ... }
for element in &mut collection { ... }
for element in collection { ... }
并非每种集合类型都提供了这 3 种实现:
HashSet、BTreeSet 和 BinaryHeap
不会在可变引用上实现IntoIterator
,因为修改它们的元素可 能会违反类型自身的不变性规则——修改后的值很可能有不同的哈希值,或者相对于其邻居的顺序改变了,所以修改它会让该类型处于错误状态。- 另一部分类型 确实支持修改,但只支持修改一部分,比如,
HashMap 和 BTreeMap
会生成对 其条目值的可变引用,但只能提供对其键的共享引用,原因与前面给出的相似。
总体原则是,迭代应该是高效且可预测的,因此 Rust 不会提供昂贵或可能表现 出意外行为的实现。
切片实现了 3 个 IntoIterator 变体中的两个,由于切片并不拥有自己的元 素,因此不存在“按值”引用的情况。&[T]
和 &mut [T]
各自的 into_iter 会 分别返回一个迭代器,该迭代器会生成对其元素的共享引用和可变引用。
为什么 Rust 要同时提供 into_iter 和 iter 这两种方式呢?
-
IntoIterator 是确保 for 循环工作的关键,是必要的。
-
但当我们 不用 for 循环时,写 favorites.iter() 会比 (&favorites).into_iter() 更清晰。我们会频繁通过共享引用进行迭代, 因此 iter 和 iter_mut 仍然具有很高的工效学价值。
IntoIterator
在泛型代码中也很有用:你可以使用像T: IntoIterator
这 样的限界来将类型变量 T 限制为可以迭代的类型,还可以编写 T: IntoIterator <Item=U>
来进一步要求迭代时生成具有特定类型 U 的条目。 例如,dump 函数可以转储任何其条目可用"{:?}"
格式打印的可迭代者的值:
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 来编写这个泛型函数,因为它们不是任何特 型的方法:只是大多数可迭代类型恰好具有叫这两个名字的方法而已。
from_fn来源 与 successors后继
from_fn:
要生成一系列值,可以通过提供一个能返回这些值的闭包实现。
use rand::random; // 在Cargo.toml中添加dependencies: rand = "0.7"
use std::iter::from_fn;
// 产生1000条端点均匀分布在区间[0, 1]上的随机线段的长度(这并不是
// `rand_distr` crate中能找到的分布类型,但你可以轻易实现一个)
let lengths: Vec<f64> =
from_fn(|| Some((random::<f64>() - random::<f64>()).abs())).take(1000).collect();
调用 from_fn 来让迭代器产生随机数。由于迭代器总是返回 Some,因此序列永不结束,但我们调用 take(1000) 时会将其限制为前 1000 个元素。然 后 collect 会从这 1000 次迭代中构建出向量
如果每个条目都依赖于其前一个条目,那么 std::iter::successors
函数 很实用。只需要提供一个初始条目和一个函数,且该函数能接受一个条目并返回 下一个条目的 Option。如果返回 None,则迭代结束。例如,下面是编写第 2 章中的曼德博集绘图器的 escape_time 函数的另一种方式:
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)
}
从零开始,successors(后继者)调用会通过反复对最后一个点求平方再加上 参数 c 来生成复平面上的一系列点。调用 take(limit) 确定了我们追踪序列的次数限制,然后 enumerate 对每个点进行编号,将每个点 z 变成元组 (i, z)。我们使用 find 来寻找距离原点足够远的第一个点以判断是否逃逸。 find 方法会返回一个 Option:如果这样的点存在就返回 Some((i, z)),否 则返回 None。调用 Option::map 会将 Some((i, z)) 变成 Some(i),但不会改变 None,因为这正是我们想要的返回值。
from_fn 和 successors 都接受 FnMut 闭包,因此你的闭包可以捕获和修改 周边作用域中的变量
斐波那契:
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) //返回第n个值})
//返回from_fn方法
}
assert_eq!(fibonacci().take(8).collect::<Vec<_>>(),vec![1, 1, 2, 3, 5, 8, 13, 21]);
drain 抽取
有许多集合类型提供了 drain(抽取)方法。drain 会接受一个对集合的可变引用,并返回一个迭代器,该迭代器会将每个元素的所有权传给消费者。然而, 与按值获取并消耗掉集合的 into_iter() 方法不同,drain 只会借入对集合 的可变引用,当迭代器被丢弃时,它会从集合中移除所有剩余元素以清空集合。 对于可以按范围索引的类型(如 String、向量和 VecDeque),drain 方法 可指定要移除的元素范围,而不是“抽干”整个序列:
let mut outer = "Earth".to_string();
let inner = String::from_iter(outer.drain(1..4));
assert_eq!(outer, "Eh");
assert_eq!(inner, "art");
“抽干”整个序列,使用整个范围(..)
作为参数即可
map转换 与 filter取舍
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;
-
Iterator 特型的 map(映射)适配器能针对迭代器的各个条目调用闭包来帮你转换迭代器。用于对迭代器中的每个元素应用一个函数,并生成一个新的迭代器,该迭代器包含应用函数后的结果。换句话说,它用来转换每个元素而不改变元素的数量。
-
filter 适配器能使用闭包来帮你从迭代器中过滤某些条目,由 闭包决定保留和丢弃哪些条目。 用于根据给定的条件(由闭包定义)来选择性地保留迭代器中的元素。只有当闭包返回
true
时,对应的元素才会被包含在新的迭代器中;如果闭包返回false
,则该元素会被排除。
假设你正在逐行遍历文本并希望去掉每一行的前导空格和尾随空格。标准库的 str::trim
方法能从单个 &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 并将 生成的结果作为自己的条目。
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"
返回 true 的那些条目。迭代器的适配器链条就像Unix shell
中的管道:每个适配器都有单一用途,并且很清楚此序列是如何在从 左到右读取时进行转换的。
在标准库中,map 和 filter 实际上返回的是名为 std::iter::Map
和std::iter::Filter
的专用不透明(隐藏了实现细节的)struct 类型。这些名字并不能告诉我们更多信息,所以我们会写成 - > impl Iterator,因为这揭示了我们真正关心的事情:此方法 返回了能生成给定类型条目的 Iterator。
大多数适配器会按值接受 self,这就要求 Self 必须是固定大小的(Sized) (所有常见的迭代器都是这样的)。
map 迭代器会按值将每个条目传给闭包,然后将闭包结果的所有权转移给自己的 消费者。filter 迭代器会通过共享引用将每个条目传给闭包,并保留所有权以便再把选定的条目传给自己的消费者。这就是为什么该示例必须解引用 s 以便 将其与 “iguanas” 进行比较:filter 迭代器的条目类型是 &str,因此闭包 参数 s 的类型是 &&str【二级指针】。
关于迭代器适配器,有两点需要特别注意:
-
单纯在迭代器上调用适配器并不会消耗任何条目,只会返回一个新的迭代器,新迭代器会根据需要从第一个迭代器中提取条目,以生成自己的条目。在适配器的适配链中,实际完成任何工作(同时消耗条目)的唯一方法是在 最终的迭代器上调用 next。 因此,调用 text.lines() 本身实际上并不会解析 字符串中的任何一行,它只是返回了一个迭代器,当需要时才会解析这些行。同样,map 和 filter 也只会返回新的迭代器,当需要时,它们才会映射或过滤。在由 collect 调用 filter 迭代器上的 next 之前,不会进行任何实际的 工作。 如果你在使用有副作用的适配器,这一点尤为重要。例如,以下代码根本不会输 出任何内容:
["earth", "water", "air", "fire"].iter().map(|elt| println!("{}", elt));
iter 调用会返回数组元素的迭代器,map 调用会返回第二个迭代器,第二个迭 代器会对第一个迭代器生成的每个值调用闭包。但是这里没有任何代码会实际用 到整个链条的值,所以 next 方法永远不会执行。
迭代器适配器的工作原理
- 不消耗元素: 当你对一个迭代器应用一个适配器时,不会立即对迭代器中的元素进行任何操作。适配器只是返回了一个新的迭代器,这个新的迭代器会在需要的时候从原始迭代器中获取元素,并根据适配器的规则进行转换或过滤。
- 延迟计算: 直到你在最终的迭代器上调用
next
方法时,适配器链才会真正开始工作,对元素进行处理。
["earth", "water", "air", "fire"].iter().map(|elt| println!("{}", elt));
为什么会出现“什么都不输出”的情况?
- 没有实际使用迭代器: 在上面的例子中,虽然创建了多个迭代器,但并没有代码真正去遍历这个迭代器链。
map
操作只是创建了一个新的迭代器,这个迭代器会对每个元素应用println!
函数,但是由于没有调用next
方法,所以println!
函数并没有被执行。
总结
- 迭代器适配器提供了一种灵活的方式来处理迭代器中的元素,但它们是惰性的。
- 只有当你在最终的迭代器上调用
next
方法时,适配器链才会真正开始工作。 - 如果想要看到输出结果,需要在最终的迭代器上调用
next
方法或者使用for
循环来遍历整个迭代器。
let v = vec!["earth", "water", "air", "fire"];
// 创建一个迭代器,并应用 map 适配器
let mapped_iter = v.iter().map(|x| x.len());
// 此时,还没有任何元素被处理
// 遍历迭代器,输出每个元素的长度
for len in mapped_iter {
println!("{}", len);
}
-
第二个要点是,迭代器的适配器是一种零成本抽象。由于 map、filter 和其他 类似的适配器都是泛型的,因此将它们应用于迭代器就会专门针对所涉及的特定迭代器类型生成特化代码。这意味着 Rust 会有足够的信息将每个迭代器的 next 方法内联到它的消费者中,然后将这一组功能作为一个单元翻译成机器代码。因此,我们之前展示的迭代器的
lines/ map/ filter 链条
会和手写代码 一样高效:for line in text.lines() { let line = line.trim(); if line != "iguanas" { `v.push(line); } }
filter_map转换并取舍 与 flat_map转换并展平收集
filter_map
filter_map
接受一个闭包作为参数,该闭包对每个元素进行映射,并且可以选择性地过滤掉某些元素。具体来说,闭包应该返回一个 Option<T>
类型:
- 如果闭包返回
Some(value)
,那么value
会被包含在结果迭代器中。 - 如果闭包返回
None
,则对应的元素会被从结果迭代器中排除。
这是当你想要同时映射和条件性地过滤元素时非常有用的方法。
示例:
let numbers = vec![1, 2, 3, 4, 5];
let mapped_and_filtered: Vec<i32> = numbers.into_iter()
.filter_map(|x| if x % 2 == 0 { Some(x * 2) } else { None })
.collect();
println!("{:?}", mapped_and_filtered); // 输出 [4, 8]
在这个例子中,只有偶数被乘以 2 并保留在新的向量中;奇数则被过滤掉了。
flat_map
flat_map
同样接受一个闭包作为参数,但是它的目的是将每个元素映射到一个新的迭代器,然后将这些迭代器展平成一个单一的迭代器。换句话说,flat_map
可以用来处理嵌套的迭代器结构。
flat_map
的行为与 map().flatten()
组合使用是等价的。这意味着你首先映射每个元素到一个新值(可以是另一个迭代器),然后通过 flatten
将所有内部迭代器合并为一个单一的迭代器。
示例:
let nested_vec = vec![vec![1, 2], vec![3, 4], vec![5]];
let flattened: Vec<i32> = nested_vec.into_iter()
.flat_map(|inner_vec| inner_vec.into_iter())
.collect();
println!("{:?}", flattened); // 输出 [1, 2, 3, 4, 5]
这里,flat_map
被用来将二维向量转换为一维向量。
总结来说,filter_map
主要用于有条件地映射并过滤元素,而 flat_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());
}
传给 filter_map 的闭包会尝试使用 f64::from_str
来解析每个以空格分隔 的切片。结果是一个 Result,再调用 .ok() 就 会把它变成 Option:如果解析错误就会变成 None;如果成功就会变成 Some(v)。filter_map 迭代器会丢弃所有 None 值并为每个 Some(v) 生成值 v。
只用 filter 和 map 也可以做同样的事:
text.split_whitespace()
.map(|w| f64::from_str(w))
.filter(|r| r.is_ok())
.map(|r| r.unwrap())
fn flat_map<U, F>(self, f: F) -> impl Iterator<Item=U::Item>
where F: FnMut(Self::Item) -> U, U: IntoIterator;
传给 flat_map 的闭包必须返回一个可迭代者,但可以返回任意种类的可迭代者
由于 Option 也是一个可迭代者,其行为类似于有零个或一个条目的序列,因此iterator.filter_map(closure)
等效于 iterator.flat_map(closure)
,这里假设 closure 会返回一个 Option。
将国家映射成其主要城市的表,遍历主要城市:
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("China", vec!["ShangHai", "HongKong"]);
major_cities.insert("The Netherlands", vec!["Amsterdam", "Utrecht"]);
let countries = ["Japan", "Brazil", "China"];
for &city in countries.iter().flat_map(|country| &major_cities[country]) {
println!("{}", city);
}
迭代器是惰性的:只有当 for 循环调用了 flat_map 迭代器的 next 方法时才会实际工作。完整的串联序列从未在内存中构建过。不过,这里 有一个小小的状态机,它会从城市迭代器中逐个提取,直到用完,然后才为下一 个国家/ 地区生成一个新的城市迭代器。其效果实际上和嵌套循环一样,但封装 成了迭代器以便使用。
flatten展平并收集
flatten(展平)适配器会串联起迭代器的各个条目,这里假设每个条目本身都是可迭代者:
用于将嵌套的迭代器展平成一个单一的迭代器。具体来说,如果有一个由多个迭代器组成的迭代器(即每个元素本身也是一个迭代器),那么你可以使用 flatten()
方法来创建一个新的迭代器,该迭代器会遍历所有内部迭代器中的元素,就像它们来自一个单独的连续序列一样。
let nested_vec = vec![vec![1, 2, 3], vec![4, 5], vec![6]];
let flattened: Vec<_> = nested_vec.into_iter().flatten().collect();
println!("{:?}", flattened); // 输出 [1, 2, 3, 4, 5, 6]
在这个例子中,nested_vec
是一个包含三个向量的向量,而 flatten()
方法被用来创建一个新的迭代器,它按顺序遍历这些内部向量的所有元素,并且 collect()
方法用于收集这些元素到一个新的 Vec
中。
值得注意的是,Rust 的 flatten
方法也可以与其他适配器结合使用,比如 filter_map
,这可以让你有条件地展开某些元素或转换元素的同时进行展平操作。
此外,Option
和 Result
类型也实现了 flatten
方法,用于减少嵌套层级。例如,如果你有一个 Option<Option<T>>
,你可以调用 flatten
来得到一个 Option<T>
,移除了一层嵌套。同样的规则适用于 Result
类型。
let opt_opt = Some(Some(42));
let single_opt: Option<i32> = opt_opt.flatten();
println!("{:?}", single_opt); // 输出 Some(42)
assert_eq!(vec![None, Some("day"), None, Some("one")]
.into_iter()
.flatten()
.collect::<Vec<_>>(),
vec!["day", "one"]);
use std::collections::BTreeMap;
// 一个把城市映射为城市中停车场的表格:每个值都是一个向量
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"]);
// 构建一个表示全部停车场的向量。`values`给出了一个能生成向量的迭代器,然后`flatten`会依次生成每个向量的元素
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"]);
这是因为 Option 本身也实现了 IntoIterator,表示由 0 个或 1 个元素组成 的序列。None 元素对迭代没有任何贡献,而每个 Some 元素都会贡献一个值。 同样,也可以用 flatten 来迭代 Option<Vec<...>>
值:其中 None 的行为 等同于空向量。
Result 也实现了 IntoIterator,其中 Err 表示一个空序列,因此将 flatten 应用于 Result 值的迭代器有效地排除了所有 Err 并将它们丢弃,进而产生了一个解包装过的由成功值组成的流。通常不应该忽略代码中的错误,但 如果你很清楚自己在做什么,那么这可能是个有用的小技巧。
发现自己随手用了 flatten,这个时候你真正需要的可能是 flat_map。例如,标准库的 str::to_uppercase
方法可以将字符串转换为大写,其工作方式如下所示:
fn to_uppercase(&self) -> String {
self.chars().map(char::to_uppercase).flatten() // 使用flat_map更好
.collect()
}
用 flatten 的原因是 ch.to_uppercase()
返回的不是单个字符,而是会生 成一个或多个字符的迭代器。将每个字符都映射成其对应的大写字母会生成由字符迭代器组成的迭代器,而 flatten 负责将它们拼接在一起,形成我们最终可以收集(collect)到 String 中的内容。 不过 map 和 flatten 的组合非常常见,因此 Iterator 为这种情况提供了 flat_map 适配器。(事实上,flat_map 比 flatten 先纳入标准库。)因此,可以把前面的代码改写成如下形式:
fn to_uppercase(&self) -> String {
self.chars()
.flat_map(char::to_uppercase)
.collect()
}
take 与 take_while取出
Iterator 特型的 take(取出)适配器和 take_while(当……时取出)适配器的作用是当条目达到一定数量或闭包决定中止时结束迭代。它们的签名如下所 示:
fn take(self, n: usize) -> impl Iterator<Item=Self::Item>
where Self: Sized;
fn take_while<P>(self, predicate: P) -> impl Iterator<Item=Self::Item>
where Self: Sized, P: FnMut(&Self::Item) -> bool;
两者都会接手某个迭代器的所有权并返回一个新的迭代器,新的迭代器会从第一 个迭代器中传递条目,并可能提早终止序列。
- take 迭代器会在最多生成 n 个条 目后返回 None。
- take_while 迭代器会针对每个条目调用
predicate
,并对predicate
返回了 false 的首个条目以及其后的每个条目都返回 None。
给定一封电子邮件,其中有一个空行将标题与邮件正文分隔开,如果只想遍历标题就可以使用 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);
}
skip 与 skip_while跳过
Iterator 特型的 skip(跳过)和 skip_while(当……时跳过)是与 take 和 take_while 互补的方法:skip 从迭代开始时就丢弃一定数量的条目, skip_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
函数会返回一个迭代器,该迭代器会将程序的各个参数生成 为一些 String 型条目,首个条目是程序本身的名称。对该迭代器调用 skip(1) 会返回一个新的迭代器,新迭代器会在首次调用时丢弃第一条:程序名称,然后生成所有后续参数。
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 body in message.lines()
.skip_while(|l| !l.is_empty()) .skip(1) {
println!("{}" , body);
}
这会使用 skip_while 来跳过非空行,但迭代器本身还是会生成一个空行—— 毕竟,闭包对该空行返回了 false。所以我们还要使用 skip 方法来丢弃它, 并返回一个迭代器,其第一个条目是消息正文的第 1 行。
- lines():将字符串分割成多个行,并返回一个迭代器,每个元素是一个单独的行。
- skip_while(|l| !l.is_empty()):跳过所有非空行(即跳过那些不为空的行)。这里使用了 skip_while 方法,它会跳过满足条件的所有元素。闭包 |l| !l.is_empty() 检查每一行是否为空,如果行不为空,则继续跳过。
- skip(1):跳过下一个元素。这意味着在跳过所有非空行之后,再跳过一行。这通常用于跳过一些不需要的行,比如邮件中的标题行。
peekable可窥视
peekable功能是允许我们窥视即将生成的下一个条目,而 无须实际消耗它。
调用 Iterator 特型的 peekable 方法可以将任何迭代器变 成 peekable 迭代器: fn peekable(self) -> std::iter::Peekable where Self: Sized;
Peekable 是一个实现了 Iterator 的结构体,而 Self 是底层迭代器的类型。 peekable 迭代器有一个额外的方法 peek,该方法会返回一个 Option<&Item>
:如果底层迭代器已耗尽,那么返回值就为 None;否则为 Some®,其中 r 是对下一个条目的共享引用。(注意,如果迭代器的条目类型已经是对某个值的引用了,则最终产出就会是对引用的引用。)
调用 peek 会尝试从底层迭代器中提取下一个条目,如果条目存在,就将其缓存,直到下一次调用 next 时给出。Peekable 上的所有其他 Iterator 方法 都知道这个缓存,比如,peekable 迭代器 iter 上的 iter.last() 就知道 要在耗尽底层迭代器后检查此缓存。
有时候,只有超前一点儿才能决定应该从迭代器中消耗多少个条目,在这种情况 下,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), 我们将返回已解析的数值并将下一个字符留在迭代器中,以供使用
fuse保险丝
Iterator 特型并没有规定一旦 next 返回 None 之后,再次调用 next 方法 时应该如何行动。你的代 码依赖于“再次返回 None”这种行为,那么遇到例外可能会让你大吃一惊。
fuse(保险丝)适配器能接受任何迭代器并生成一个确保在第一次返回 None 后继续返回 None 的迭代器:
当迭代器达到其末尾时,调用 fuse()
方法会返回一个新的迭代器,这个新的迭代器在首次遇到 None
后将永远继续返回 None
,即使原始的迭代器后来又变得可用(例如,在某些情况下,迭代器可能被重置或再次填充)。
这就好比是一个保险丝:一旦电流(在这里是指向下一个元素的操作)中断,保险丝就会熔断,并阻止任何进一步的电流通过,直到保险丝被替换(在这种情况下,就是创建一个新的迭代器)。
使用场景
- 防止多次结束:确保迭代器不会在已经完成一次遍历后还能产生新的值。
- 简化逻辑:当你希望在迭代器结束时立即停止处理,而不必担心后续操作可能会意外地再次产生值。
let mut iter = (0..3).cycle().take(5).fuse();
while let Some(v) = iter.next() {
println!("{}", v);
}
在这个例子中,我们首先创建了一个无限循环的迭代器 (0..3).cycle()
,然后使用 take(5)
来限制只取前五个元素。但是,由于 cycle
的存在,如果没有 fuse
,理论上迭代器可以再次开始从头输出值。然而,因为我们调用了 fuse()
,所以在第五次迭代之后,迭代器将不再产生任何新值,即它会在第一次遇到 None
后“熔断”。
fuse
对于编写健壮的代码非常重要,特别是在你不能确定迭代器的行为或者想要确保迭代器在完成一次遍历后不再产生新的值的情况下。它可以避免一些潜在的逻辑错误和不必要的复杂性。
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("totally the last item")
} else {
self.0 = true; // 糟糕!
None
}
}
}
let mut flaky = Flaky(true);
assert_eq!(flaky.next(), Some("totally the last item"));
assert_eq!(flaky.next(), None);
assert_eq!(flaky.next(), Some("totally the last item"));
let mut not_flaky = Flaky(true).fuse();
assert_eq!(not_flaky.next(), Some("totally the last item"));
assert_eq!(not_flaky.next(), None);
assert_eq!(not_flaky.next(), None);
fuse 适配器在需要使用不明来源迭代器的泛型代码中非常有用。与其奢望要处 理的每个迭代器都表现良好,还不如使用 fuse 加上保险
可逆迭代器与 rev翻转
有的迭代器能够从序列的两端抽取条目,使用 rev(逆转)适配器可以反转此类迭代器。例如,向量上的迭代器就可以像从头开始一样轻松地从向量的末尾抽取 条目。这样的迭代器可以实现std::iter::DoubleEndedIterator
特型, 它扩展了 Iterator:
trait DoubleEndedIterator: Iterator {
fn next_back(&mut self) -> Option<Self::Item>;
}
你可以将双端迭代器想象成用两根手指分别标记序列的当前首端和尾端。从任何 一端提取条目都会让该手指向另一端前进,当两者相遇时,迭代就完成了:
let bee_parts = ["head", "thorax", "abdomen"];
let mut iter = bee_parts.iter();
assert_eq!(iter.next(), Some(&"head"));
assert_eq!(iter.next_back(), Some(&"abdomen"));
assert_eq!(iter.next(), Some(&"thorax"));
assert_eq!(iter.next_back(), None);
assert_eq!(iter.next(), None);
切片迭代器实际上是一对 指向我们尚未生成的元素范围的起始指针和结束指针,next 和 next_back 所 做的只是从起始指针或结束指针中提取一个条目而已。BTreeSet 和 BTreeMap 等有序集合的迭代器也是双端的:它们的 next_back 方法会首先提 取最大的元素或条目。总体而言,只要有可能,标准库就会提供双端迭代能力。
如果迭代器是双端的,就可以用 rev 适配器将其逆转,返回的迭代器也是双端的,只是互换了 next 方法和 next_back 方法:
fn rev(self) -> impl Iterator<Item=Self>
where Self: Sized + DoubleEndedIterator;
inspect检查
inspect(探查)适配器为调试迭代器适配器的流水线提供了便利,但在生产代码中用得不多。inspect 只是对每个条目的共享引用调用闭包,然后传递该条目。闭包不会影响条目,但可以做一些事情,比如打印它们或对它们进行断 言。
将字符串转换为大写会更改其长度的情况
let upper_case: String = "große".chars()
.inspect(|c| println!("before: {:?}", c))
.flat_map(|c| c.to_uppercase())
.inspect(|c| println!(" after: {:?}", c))
.collect();
assert_eq!(upper_case, "GROSSE");
# 输出
before: 'g'
after: 'G'
before: 'r'
after: 'R'
before: 'o'
after: 'O'
before: 'ß'
after: 'S'
after: 'S'
before: 'e'
after: 'E'
chain链接
chain(链接)适配器会将一个迭代器追加到另一个迭代器之后。更准确地说, i1.chain(i2) 会返回一个迭代器,该迭代器从 i1 中提取条目,直到用尽, 然后从 i2 中提取条目。
fn chain<U>(self, other: U) -> impl Iterator<Item=Self::Item>
where Self: Sized, U: IntoIterator<Item=Self::Item>;
可以将迭代器与任何会生成相同条目类型的可迭代者链接在一起,如果 chain 的两个底层迭代器都是可逆的,则其结果迭代器也是可逆的
enumerate枚举
Iterator 特型的 enumerate(枚举)适配器会将运行索引附加到序列中,它接受某个迭代器生成的条目 A, B, C, … 并返回生成的值对 (0, A), (1, B), (2, C), …。乍看起来,这微不足道,但其使用频率相当惊人。
消费者可以使用上述索引将一个条目与另一个条目区分开来,并建立处理每个条目时的上下文。将图像分成 8 个水平条带,并将每个条带分配给不同的线程。可以使用enumerate
来告诉每个 线程其条带对应于图像的哪个部分。
一个矩阵像素缓冲区:
let mut pixels = vec![0; columns * rows];
使用 chunks_mut 将图像拆分为一些水平条带,每个线程负责一个
let threads = 8;
let band_rows = rows / threads + 1;
...
let bands: Vec<&mut [u8]> = pixels.chunks_mut(band_rows * columns).collect();
遍历条带,为每个条带启动一个线程:
for (i, band) in bands.into_iter().enumerate() {
let top = band_rows * i;
// 启动一个线程来渲染`top..top + band_rows`范围内的行
...
}
每次迭代都会得到一个 (i, band) 值对,其中 band 是 &mut [u8] 类型的像 素缓冲区切片,表示线程应该绘制的区域,而 i 是该条带在整个图像中的索 引,由 enumerate 适配器提供。绘图的边界和条带的大小足以让线程确定分配 给它的是图像中的哪个部分,从而确定要在 band 中绘制什么。
可以将 enumerate 生成的 (index, item) 值对视为在迭代 HashMap 或其 他关联集合时获得的 (key, value) 值对。如果在切片或向量上进行迭代,则 index 就是 item 对应的 key。
zip合体
zip(拉合)适配器会将两个迭代器组合成一个迭代器,新的迭代器会生成值对,每个底层迭代器各提供一个值,就像把拉链的两侧拉合起来一样。
当两个底层迭代器中的任何一个已结束时,拉合后的迭代器就结束了。
例如,可以通过将无尽范围 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 = ["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!("Headers:");
for header in lines.by_ref().take_while(|l| !l.is_empty()) { //None空行迭代器的可变引用消耗
println!("{}" , header);
}
println!("\nBody:");
for body in lines {
println!("{}" , body);
}
调用 lines.by_ref() 会借出一个对迭代器的可变引用,take_while 迭代 器会取得这个引用的所有权。该迭代器在第一个 for 循环结束时超出了作用域,表示本次借用已结束,这样你就能在第二个 for 循环中再次使用 lines 了。上述代码将输出以下内容:
Headers:
To: jimb
From: id
Body:
Oooooh, donuts!!
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:
let a = ['1', '2', '3', '∞'];
assert_eq!(a.iter().next(), Some(&'1'));
assert_eq!(a.iter().cloned().next(), Some('1'));
copied(复制后)适配器限制更严格,它要求引用目 标的类型必须实现了 Copy。像 iter.copied() 这样的调用与 iter.map(|r| *r)
大致相同。
由于每个实现了 Copy 的类型也必定实现了 Clone,因此 cloned 更通用。但根据条目类型的不同,clone 调用可能会进行任意次数的分配和复制。如果你认为由于条目类型很简单,因而永远不会发生内存分配,那么最好使用 copied。
cycle循环迭代
cycle(循环)适配器会返回一个迭代器,它会无限重复底层迭代器生成的序 列。底层迭代器必须实现 std::clone::Clone,以便 cycle 保存其初始状态 并且在每次循环重新开始时复用它。下面是一个例子:
let dirs = ["North", "East", "South", "West"];
let mut spin = dirs.iter().cycle();
assert_eq!(spin.next(), Some(&"North"));
assert_eq!(spin.next(), Some(&"East"));
assert_eq!(spin.next(), Some(&"South"));
assert_eq!(spin.next(), Some(&"West"));
assert_eq!(spin.next(), Some(&"North"));
assert_eq!(spin.next(), Some(&"East"));
玩家轮流数数,将任何可被 3 整除的数值替换为单词 fizz;将任何可被 5 整除的数值替换 为单词 buzz;能被两者整除的数值则替换为单词 fizzbuzz。
use std::iter::{once, repeat};
let fizzes = repeat("").take(2).chain(once("fizz")).cycle();
let buzzes = repeat("").take(4).chain(once("buzz")).cycle();
let fizzes_buzzes = fizzes.zip(buzzes);
let fizz_buzz = (1..100).zip(fizzes_buzzes)
.map(|tuple|
match tuple {
(i, ("", "")) => i.to_string(),
(_, (fizz, buzz)) => format!("{}{}", fizz, buzz)
});
for line in fizz_buzz {
println!("{}", line);
}