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 循环其实只是一种简写,其底层调用了 IntoIteratorIterator 的方法:

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

大多数集合类型提供了 iteriter_mut 方法,它们返回该集合类型的迭代器,迭代器产生对每个 item 的共享引用或可变引用。像数组切片 &[T]&mut [T] 也有iteriter_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 的末端。

每种类型都可以依照其目的自由地实现 iteriter_mutstd::path::Pathiter 方法返回一个迭代器,每次迭代产生一个 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 变体——共享引用和可变引用——相当于在引用上调用 iteriter_mut。为什么 Rust 同时提供两种方式呢?

IntoIterator 是使 for 循环工作的底层保障,显然是必要的。但是当你不使用 for 循环时,写 favorites.iter() 比写 (&favorites).into_iter() 会更清楚。你经常需要通过共享引用进行迭代,所以 iteriter_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);
    }
}

你不能用 iteriter_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_fnsuccessors 都接受 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_fnsuccessor 方法足够灵活,致使你可以通过复杂的闭包,把对迭代器的任何调用变成对一个元素的单一调用。但这样做却忽视了迭代器的价值——即明确数据如何在计算中流转,并为常见模式使用标准命名。在使用这两个迭代器之前,请确保你已经熟悉了其他迭代器方法,因为通常有更好的方法来完成工作。

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 提供了大量的适配器方法,或者简单说成适配器,它们可以消费一个迭代器并建立另一个新迭代器,新的迭代器具有一些实用功能。为了了解适配器如何工作,我们将从两个最流行的适配器开始——即 mapfilter。然后继续介绍适配器工具箱的其余部分,包括几乎所有你能想到的从其他序列中生成数值序列的方法:截断、跳过、组合、反转、连接、重复等等。

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;

在标准库中,mapfilter 实际上返回了特定的不透明数据类型(opaque struct),分别为 std::iter::Mapstd::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() 方法本身实际上并不解析字符串中任何行。该方法只是返回了一个迭代器,只有在需要时才会开始解析各行。同样,mapfilter 也只是返回一个新的迭代器,只有在需要时才会进行映射或过滤。在 collect 开始调用 filter 迭代器的 next 之前,不会有任何迭代工作发生。

如果所使用的适配器有副作用,这一点尤为重要。例如,这段代码根本不会打印任何内容:

["earth", "water", "air", "fire"]
    .iter().map(|elt| println!("{}", elt));

调用 iter 返回数组元素的迭代器,再调用 map 返回第二个迭代器,它将闭包应用于第一个迭代器所产生的每个值。但是,此时并没有从整个调用链中实际要求一个值,没有运行 next 方法。事实上,Rust 会对此给出警告:

警告信息中的 "lazy" 一词并非贬义。它是一种行话,指任何将计算推迟到需要其值时才进行的机制。按照 Rust 的惯例,迭代器应该在每次调用 next 时做最少的运算,本例中根本没有这样的调用,所以没有运算。

第二个要点是,迭代器适配器是一种零成本抽象。 mapfilter 和他们的其它小伙伴都是非常常用的方法,因此将它们应用于迭代器时,会针对所涉及的特定迭代器类型对代码进行特殊处理。这意味着 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_mapflat_map 适配器应运而生。

filter_map 适配器与 map 类似,只是它除了可以通过闭包函数将某项转换为新项(类同 map 的功能)外,还可以将项从迭代中删除。因此,它有点像 filtermap 的结合体。其签名如下:

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.000.25289.003.14)

传入 filter_map 的闭包尝试用 f64::from_str 解析每个以空白分隔的单词,然后返回 一个 Result<f64,ParseFloatError>.ok() 再将其转化为一个 Option<f64>:解析失败的结果为 None,而解析成功的结果为 Some(v)filter_map 迭代器会删除所有 None 值,并为每个 Some(v) 生成值 v

但是,像这样把 mapfilter 融合到一个操作中,而不是直接使用这些适配器,又有什么意义呢?filter_map 适配程序在类似刚才的情况下显示出了其价值,在这种情况下,决定是否将某个项包含在迭代中的最佳方法就是尝试实际去处理它。只用 filtermap 也能做同样的事情,但有点不够优雅:

text.split_whitespace()
    .map(|w| f64::from_str(w))
    .filter(|r| r.is_ok())
    .map(|r| r.unwrap())

flat_mapmapfilter_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 个值。同样,你也可以使用 flattenOption<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 会将这些字符迭代器拼接起来,最终 collectString

这种 mapflaten 的组合非常常见,因此 Iterator 提供了 flat_map 适配器来处理这种情况(其实 flat_mapflatten 更早加入标准库):

fn to_uppercase(&self) -> String {
    self.chars()
        .flat_map(char::to_uppercase)
        .collect()
}

take 和 take_while

Iterator trait 中的 taketake_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 个项,之后则返回 Nonetake_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 的 skipskip_while 方法是对 taketake_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 会尝试从底层迭代器中抽取下一个项,若存在,则将它缓存起来直到下次调用 nextPeekable 上的其他所有 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);

切片上迭代器的结构使得该行为很容易实现:实际上它是一对指针,分别指向尚未产生的元素的起点和终点;nextnext_back 只需从其中之一提取一个项即可。有序集合(如 BTreeSetBTreeMap)的迭代器也是双端的:它们的 next_back 方法会首先提取最大的元素。一般来说,在可行的情况下标准库都会提供双端迭代。

然而并不是所有的迭代器都能轻易实现该行为:一个从其他线程产生值的迭代器到达通道的 Receiver 时,无法预知最后接收到的值是什么。一般来说,你需要查看标准库的文档,以了解哪些迭代器实现了 DoubleEndedIterator,哪些没有。

如果迭代器是双端的,可以使用 rev 适配器将其反转:

fn rev(self) -> impl Iterator<Item=Self>
    where Self: Sized + DoubleEndedIterator;

rev 返回的迭代器也是双端的,其实它只是把 nextnext_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_mapto_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,并根据需要在其中一个迭代器上调用 nextnext_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_whileskip_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 也是一个迭代器,其 nextsize_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,以便类型检查器进行检查。


Qiang
271 声望25 粉丝

Hello segmentfault


引用和评论

0 条评论