课程概览 · 第 12 章

上一篇:集合:从访问模式选择 Vec、Map、Set 与队列

下一篇:错误处理:区分可恢复失败与程序缺陷

第 11 章讨论数据放在哪里,本章讨论如何转换数据。迭代器描述逐项读取、过滤与映射,闭包提供转换逻辑及其需要的环境。读一条管道时,依次确认入口的所有权、计算的驱动点和结果是否借用输入;这比记住许多方法名更重要。

迭代器由 next 驱动,闭包调用能力取决于如何使用捕获值,move 改变捕获方式
图:分开判断计算何时发生、输入归谁、闭包如何使用环境。

前置知识与学习目标

需要理解借用、生命周期与集合基本操作。学完本章应能:

  • 预测 iter、iter_mut、into_iter 的元素类型与所有权后果;
  • 用 next 观察惰性,选择适配器、消费者和有限收口方式;
  • 区分借用结果与拥有结果,明确 collect 的目标类型;
  • 根据调用方式选择 Fn、FnMut、FnOnce,而不是根据是否写 move 判断。

迭代入口决定 Item

对 Vec 和数组,iter() 产出 &T,iter_mut() 产出 &mut T,按值 into_iter() 产出 T。更一般地,IntoIterator 的 Item 取决于被迭代的类型:(&tasks).into_iter() 也只是借用,不是把任务移走。for 使用的是 IntoIterator,并不天然偏向借用或拥有。

独立示例(仅标准库;完整内容放 src/main.rs,Edition 2024;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Task {
title: String,
done: bool,
}

fn main() {
let mut tasks = vec![
Task { title: "写发布说明".into(), done: false },
Task { title: "升级依赖".into(), done: true },
];
let titles: Vec<&str> = tasks.iter().map(|t| t.title.as_str()).collect();
assert_eq!(titles, ["写发布说明", "升级依赖"]);
for task in tasks.iter_mut().filter(|t| !t.done) {
task.title.push_str(" [待办]");
}
assert_eq!(tasks[0].title, "写发布说明 [待办]");
let owned: Vec<Task> = tasks.into_iter().filter(|t| t.done).collect();
assert_eq!(owned[0].title, "升级依赖");
}

数组从 Rust 1.53 起实现 IntoIterator;2021 之前的 edition 为兼容旧代码,对 array.into_iter() 方法调用保留引用迭代行为。Edition 2024 中它按值产出元素。这个历史差异不应泛化到所有 IntoIterator 实现。

next 与惰性

Iterator 的核心是关联类型 Item 与 next(&mut self) -> Option<Self::Item>。标准库的 map、filter 等适配器返回新的迭代器,构造时不遍历上游;调用 next、执行 for 或调用消费者才驱动它们。一次 filter 的 next 可能读取多个上游元素,直到找到符合条件的项。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::cell::Cell;

fn main() {
let calls = Cell::new(0u32);
let numbers = [10, 20, 30];
let mut iter = numbers.iter().map(|n| {
calls.set(calls.get() + 1);
n * 2
});
assert_eq!(calls.get(), 0);
assert_eq!(iter.next(), Some(20));
assert_eq!(calls.get(), 1);
assert_eq!(iter.next(), Some(40));
assert_eq!(iter.next(), Some(60));
assert_eq!(calls.get(), 3);
assert_eq!(iter.next(), None);
}

None 表示这一次没有项;并非所有迭代器都保证之后永远返回 None。需要这种保证时用 fuse。消费者取得迭代器的所有权通常会移动该迭代器;需要分段消费时,可以借用它或使用 by_ref()。

适配器与消费者

常用适配器

方法作用与边界
map / filter一对一转换 / 按谓词筛选;filter 的谓词接收 &Item
filter_map闭包返回 Option<U>,同时筛选与转换
flat_map展开每项产生的子迭代器
enumerate附加从 0 开始的序号
zip按短的一方结束;不会报告长度不匹配
take(n) / skip(n)限制项数 / 跳过项数;上游每次 next 仍需能返回
chain连接相同 Item 类型的两个迭代器
scan回调维护状态,返回 None 可提前结束
copied / cloned把引用元素变成值,分别要求 Copy / Clone
peekable / rev偷看下一项 / 反向读取,rev 要求双端迭代器
step_by / by_ref / fuse跨步读取 / 借用分段消费 / 固定结束状态

当 tasks.iter() 的 Item = &Task 时,filter 的参数是 &&Task;字段访问的自动解引用常把这一层隐藏。map、filter_map 的参数直接是 Item,即 &Task。不要把所有闭包参数都记成“元素引用”。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Task {
title: String,
state: &'static str,
}

fn main() {
let tasks = [
Task { title: "修复登录".into(), state: "doing" },
Task { title: "写发布说明".into(), state: "todo" },
];
let doing: Vec<&str> = tasks.iter()
.filter(|t: &&Task| t.state == "doing")
.map(|t| t.title.as_str()).collect();
assert_eq!(doing, ["修复登录"]);
let same: Vec<&str> = tasks.iter().filter_map(|t| {
(t.state == "doing").then_some(t.title.as_str())
}).collect();
assert_eq!(same, doing);
let pairs: Vec<_> = tasks.iter().zip(["小赵"]).collect();
assert_eq!(pairs.len(), 1);
let cumulative: Vec<i32> = [1, 2, 1].into_iter().scan(0, |acc, n| {
*acc += n;
Some(*acc)
}).collect();
assert_eq!(cumulative, [1, 3, 4]);
}

消费者的返回类型

方法返回与停止条件
sum / product / count数值;通常需要遍历到结束
findOption<Self::Item>;命中即停
max / min / lastOption<Self::Item>;空迭代器返回 None
any / all / position布尔值或位置;满足各自短路条件可提前停止
fold累加器值;通常遍历到结束
partition两个目标集合;按谓词分组
for_each();为副作用消费
collect实现 FromIterator 的目标;具体实现决定分配与短路

find、max、min 不会凭空添加引用:Item 是 &T 时返回 Option<&T>,Item 是 T 时返回 Option<T>。find 谓词接收 &Item,但最终交回 Item 本身。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let durations = [30u32, 45, 15, 60, 20];
assert_eq!(durations.iter().find(|d| **d >= 45), Some(&45));
assert_eq!(durations.into_iter().find(|d| *d >= 45), Some(45));
assert_eq!(durations.iter().max(), Some(&60));
assert_eq!(durations.into_iter().max(), Some(60));
let max = durations.iter().fold(0, |acc, d| acc.max(*d));
assert_eq!(max, 60);
let (quick, slow): (Vec<u32>, Vec<u32>) =
durations.into_iter().partition(|d| *d <= 30);
assert_eq!(quick, [30, 15, 20]);
assert_eq!(slow, [45, 60]);
}

collect 与有限收口

collect 根据目标的 FromIterator 实现收集,不是“唯一且必然分配”的节点:空 Vec 可能不分配,收集到 () 无需目标存储,闭包本身也可能分配。收集到 Result<Vec<T>, E> 时遇到第一个 Err 可停止;收集到普通 Vec 则要等上游结束。无法推断目标时,写 collect::<Vec<_>>() 或变量类型。

无限流必须有能证明的停止条件。无限 sum/count 不会正常完成,collect::<Vec<_>>() 可能持续分配;find/any 只有命中时提前结束,take_while 只有谓词变为 false 时结束,不能仅凭方法名保证终止。take(n) 限制产出数量,但若上游某次 next 自身永不返回,也无法解决停滞。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::collections::{HashMap, HashSet};

fn main() {
let finite: Vec<_> = std::iter::repeat(7).take(3).collect();
assert_eq!(finite, [7, 7, 7]);
let unique: HashSet<_> = [1, 2, 2, 3].into_iter().collect();
assert_eq!(unique.len(), 3);
let scores: HashMap<_, _> = [("登录", 30), ("登录", 45)]
.into_iter().collect();
assert_eq!(scores.get("登录"), Some(&45));
let grouped: HashMap<&str, Vec<&str>> = [
("doing", "修复登录"), ("todo", "写文档"), ("doing", "升级依赖"),
].into_iter().fold(HashMap::new(), |mut acc, (state, title)| {
acc.entry(state).or_default().push(title);
acc
});
assert_eq!(grouped["doing"], ["修复登录", "升级依赖"]);
let ok: Result<Vec<u32>, _> = ["1", "2", "3"]
.into_iter().map(str::parse).collect();
assert_eq!(ok.unwrap(), [1, 2, 3]);
let bad: Result<Vec<u32>, _> = ["1", "x"]
.into_iter().map(str::parse).collect();
assert!(bad.is_err());
}

HashMap 的顺序不稳定;本例按输入顺序处理重复键,后写覆盖先写。需要聚合重复键时,用 fold/循环配 entry 追加,不能指望 collect 自动合并值。

借用结果与拥有结果

生命周期只描述引用关系,不延长输入寿命。返回 Vec<&str> 会分配引用列表,却不复制标题;返回 Vec<String> 克隆标题,使输出可独立保存。若可以消费输入,也能移出 String,而不必克隆。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct Task {
title: String,
state: &'static str,
}

fn active_titles<'a>(tasks: &'a [Task]) -> Vec<&'a str> {
tasks.iter().filter(|t| t.state == "doing")
.map(|t| t.title.as_str()).collect()
}

fn active_titles_owned(tasks: &[Task]) -> Vec<String> {
tasks.iter().filter(|t| t.state == "doing")
.map(|t| t.title.clone()).collect()
}

fn main() {
let tasks = vec![
Task { title: "修复登录超时".into(), state: "doing" },
Task { title: "整理发布说明".into(), state: "todo" },
];
assert_eq!(active_titles(&tasks), ["修复登录超时"]);
let owned = active_titles_owned(&tasks);
drop(tasks);
assert_eq!(owned, ["修复登录超时"]);
}

故意失败:编译错误(仅标准库;独立内容放 src/main.rs;预期借用仍被使用时不能移走输入,通常为 E0505)。

1
2
3
4
5
6
fn main() {
let titles = vec!["登录".to_string(), "支付".to_string()];
let borrowed: Vec<&str> = titles.iter().map(String::as_str).collect();
drop(titles);
assert_eq!(borrowed.len(), 2);
}

若结果需要活得更久,改为 let owned: Vec<String> = titles.into_iter().collect();,随后使用 owned。没有必要为了避开有实际需求的拥有结果而让借用贯穿多层缓存与 API;也不能为 clone 编造统一的时间成本。

捕获与 Fn、FnMut、FnOnce

闭包参数不是捕获值。例如 filter(|t| t.state == target) 中 t 来自迭代器,target 才来自环境。编译器根据闭包体选择共享借用、可变借用或按值捕获;Copy 值的按值捕获是复制。

调用如何使用闭包环境调用能力
不需要可变访问闭包自身,也不移出非 Copy 值Fn:可通过共享借用重复调用
需要可变访问闭包自身,不移出非 Copy 值FnMut:可通过可变借用重复调用
需要移出并消耗非 Copy 捕获值仅 FnOnce:调用会消费闭包

所有闭包都实现 FnOnce;Fn 还实现 FnMut,FnMut 还实现 FnOnce。FnOnce 约束意味着调用方可以按值调用一次,不意味着所有满足它的闭包都只能调用一次。内部可变性允许 Fn 修改共享环境中的 Cell,不能把 Fn 简化为“绝不会修改任何数据”。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let prefix = String::from("任务");
let label = |id| format!("{prefix}-{id}");
assert_eq!(label(1), "任务-1");
assert_eq!(label(2), "任务-2");
let mut next_id = 100;
let mut issue_id = || {
next_id += 1;
next_id
};
assert_eq!(issue_id(), 101);
assert_eq!(issue_id(), 102);
let title = String::from("写发布说明");
let take_title = || title;
assert_eq!(take_title(), "写发布说明");
}

故意失败:编译错误(仅标准库;独立内容放 src/main.rs;第二次调用使用已移动的闭包,通常为 E0382)。

1
2
3
4
5
6
fn main() {
let title = String::from("写发布说明");
let take_title = || title;
let _ = take_title();
let _ = take_title();
}

move 与 API bound

move 强制用到的环境值按值捕获,但闭包体若只借用这些值,仍可实现 Fn。它也不把短生命周期引用变成 'static 引用。普通 thread::spawn 要求 Send + 'static,scoped 线程则可在作用域内借用,详见第 18 章。

API 应按实际调用需求选择约束:调用一次用 FnOnce;重复调用且允许改捕获状态用 FnMut;需要共享访问闭包自身时才要求 Fn。迭代器的 map/filter 使用 FnMut,因此允许累计状态,但不能每项都移出同一个 String。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn consume_once(f: impl FnOnce() -> String) -> String {
f()
}

fn call_twice(mut f: impl FnMut()) {
f();
f();
}

fn apply_twice(f: impl Fn(i32) -> i32, n: i32) -> i32 {
f(f(n))
}

fn main() {
let title = String::from("修复登录");
assert_eq!(consume_once(|| title), "修复登录");
let mut calls = 0;
call_twice(|| calls += 1);
assert_eq!(calls, 2);
assert_eq!(apply_twice(|n| n + 3, 1), 7);
let prefix = String::from("待办");
let label = move |id| format!("{prefix}-{id}");
assert_eq!(label(1), "待办-1");
assert_eq!(label(2), "待办-2");
let offset = 10;
let add = move |n| n + offset;
assert_eq!(add(2), 12);
assert_eq!(offset, 10);
}

深入阅读:生成规则与闭包类型

无集合的迭代器

std::iter 的 empty、once、repeat、repeat_with、successors、from_fn 直接从规则构造迭代器,iter::zip(Rust 1.59 起)接受两个 IntoIterator。successors 接收下一项的生成规则;from_fn 适合把外部状态接入 next。保持惰性与停止条件分析,不要因为没有集合就忽略无限流。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
use std::iter;

fn main() {
let batches: Vec<u32> = iter::successors(Some(100), |&n| {
(n < 400).then_some(n + 150)
}).collect();
assert_eq!(batches, [100, 250, 400]);
let mut backlog = vec!["登录", "支付", "导出"];
let drained: Vec<_> = iter::from_fn(|| backlog.pop()).collect();
assert_eq!(drained, ["导出", "支付", "登录"]);
let with_header: Vec<_> = iter::once("title").chain(["t1", "t2"]).collect();
assert_eq!(with_header, ["title", "t1", "t2"]);
}

异构闭包与返回闭包

每个闭包表达式有独立类型。泛型参数或 impl Fn 无需动态分发;异构集合可用 Box<dyn Fn>,借用调用也可用 &dyn Fn。Box 有拥有与分配成本,dyn 调用有间接调用;管道本身不保证比 for 快。返回单一闭包类型用 impl Fn,按运行期条件返回不同闭包类型可用 Box,也可以先考虑枚举或重写为单一闭包。

独立示例(仅标准库;替换 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn make_adder(step: i32) -> impl Fn(i32) -> i32 {
move |n| n + step
}

fn make_op(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"add" => Box::new(|a, b| a + b),
_ => Box::new(|a, b| a - b),
}
}

fn main() {
let steps: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(|n| n + 1), Box::new(|n| n * 2),
];
let mut value = 1;
for step in &steps {
value = step(value);
}
assert_eq!(value, 4);
assert_eq!(make_adder(5)(10), 15);
assert_eq!(make_op("add")(2, 3), 5);
assert_eq!(make_op("sub")(2, 3), -1);
}

返回类型 Box<dyn Fn(...)> 默认要求捕获环境满足 'static;保存借用时应显式写 + 'a 并让对象不活过输入。存储闭包或返回闭包本身并不要求装箱,是否异构与是否擦除类型才是关键。

move 到普通线程

普通 spawn 可让线程比调用者的局部作用域活得更久;把拥有的 String 移入闭包满足本例的生命周期要求,join 再等待其结果。这不是所有跨线程 API 的统一限制,scoped 线程的借用边界见第 18 章。

独立示例(仅标准库;替换独立项目 src/main.rs;执行 cargo +stable run)。

1
2
3
4
5
6
7
use std::thread;

fn main() {
let name = String::from("worker-1");
let handle = thread::spawn(move || format!("{name} started"));
assert_eq!(handle.join().expect("worker completes"), "worker-1 started");
}

自测

  1. iter().find 与 into_iter().find 的返回类型为什么不同?filter 的参数又是什么?
  2. 惰性计数例构造 map 后、调用三次 next 后,计数分别是多少?
  3. 无限 repeat 配 any 能否保证结束?collect 是否必然分配?
  4. 为什么 move 闭包能重复调用,而没有 move 的 take_title 只能调用一次?
  5. 返回借用标题与拥有标题各有什么边界?

参考答案

  1. 它们的 Item 分别为引用和值;find 返回 Option,filter 接收 &Item。
  2. 分别为 0、3;本例每次 next 映射一项。
  3. any 需要命中才短路,不保证结束;分配由目标 FromIterator 与转换逻辑决定。
  4. 调用能力取决于闭包体:只借用捕获值可重复,移出 String 会消费闭包。
  5. 借用结果不能活过输入,拥有结果可独立保存,但取得拥有值可能需要克隆或消费输入。