课程概览 · 第 12 章

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

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

第 11 章解决了"数据放哪个容器",本章解决"数据怎么转换":当一条查询要穿过过滤、映射、分组多个步骤,当转换结果要脱离输入长期保存时,手写 for 循环加中间 Vec 开始显得笨重。Rust 的答案是两个配合使用的工具:迭代器回答"转换何时执行",闭包回答"环境怎么捕获"。本章仍在任务队列/CLI 业务域里,把迭代器管道从入口选择讲到生命周期边界,把闭包从捕获方式讲到 Fn/FnMut/FnOnce

迭代适配器和消费者的惰性执行关系,以及 Fn、FnMut、FnOnce 和 move 的闭包捕获规则
图:转换管道并不立即执行;先找消费者,再检查输入的所有权与闭包对外部环境的捕获权限。

学习目标与默认选择

学完本章你应当能够:

  • 区分 iter()iter_mut()into_iter() 三种迭代入口,并说明 for 循环的展开形式;
  • 说明迭代器的惰性执行模型:适配器只包裹不计算,next() 被调用才真正驱动;
  • 选用常用惰性适配器(filter/map/filter_map/flat_map/enumerate/zip/take/skip/chain/scan)与消费者(collect/sum/fold/find/any/partition)搭建管道;
  • 写出带显式生命周期标注的迭代器管道,并解释返回值为什么不能活过输入集合;
  • 理解 collect::<Vec<_>>() 的类型推断边界,并能把结果收集到 HashMapHashSetResult<Vec<_>, _>
  • Fn/FnMut/FnOnce 描述闭包对环境的使用方式。

默认选择:迭代器和闭包是日常默认,不是优化技巧——优先用管道表达转换;只有当链条长到影响可读性时才拆成具名函数或退回 for 循环。

概念讲解:迭代器管道与生命周期

for 循环能做的,迭代器管道都能做,而且每一步都可命名、可组合。迭代器真正的学习成本不在语法,而在三件事:入口选哪个(借用还是消耗)、计算何时发生(惰性)、结果能活多久(生命周期绑定输入)。本节按这个顺序展开。

三种迭代入口:iteriter_mutinto_iter

集合本身不是迭代器,进入迭代器世界有三扇门,区别只在产出什么

  • iter():产出 &T,只读借用,集合之后还能继续使用;
  • iter_mut():产出 &mut T,可原地修改;
  • into_iter():产出 T,消耗集合本身。

for 循环只是这三扇门的语法糖:for t in &tasks 等价于 tasks.iter()for t in &mut tasks 等价于 tasks.iter_mut()for t in tasks 等价于 tasks.into_iter()——最后一种会把 Vec move 进循环:

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
#[derive(Debug)]
struct Task {
title: String,
done: bool,
}

fn main() {
let mut tasks = vec![
Task { title: "写发布说明".to_string(), done: false },
Task { title: "升级依赖".to_string(), done: true },
];

// iter():产出 &Task,原集合之后仍可使用
let titles: Vec<&str> = tasks.iter().map(|t| t.title.as_str()).collect();
assert_eq!(titles, vec!["写发布说明", "升级依赖"]);

// iter_mut():产出 &mut Task,可原地修改
for task in tasks.iter_mut().filter(|t| !t.done) {
task.title.push_str(" [待办]");
}
assert_eq!(tasks[0].title, "写发布说明 [待办]");

// into_iter():产出 Task,集合被消耗
let owned: Vec<Task> = tasks.into_iter().filter(|t| t.done).collect();
assert_eq!(owned.len(), 1);
assert_eq!(owned[0].title, "升级依赖");
// tasks 已被 move,此行之后不能再使用 tasks
}

注意数组与 Vec 的差异:vec.into_iter() 一直按值产出 T;而数组在 Rust 2021 之前 array.into_iter() 会自动退化为引用迭代(产出 &T),2021 起才统一为按值产出。读旧代码时这是一个常见的困惑点。

惰性由 next() 驱动:适配器与消费者

所有迭代器实现同一个 trait,核心只有一个方法:

1
2
3
4
5
6
// Iterator trait 的核心(简化):
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// map/filter/collect/… 都是建立在 next 之上的默认方法
}

mapfilter 这类适配器返回的是包裹上游的新迭代器,本身一次计算都不做;只有 next() 被调用,链条才真正流动。用一个计数器可以精确观察到这一点:

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

fn main() {
let calls = Cell::new(0u32);
let numbers = vec![10, 20, 30];

// map 只是包裹了一层:闭包一次都没执行
let mut iter = numbers.iter().map(|n| {
calls.set(calls.get() + 1);
n * 2
});
assert_eq!(calls.get(), 0);

// next() 被调用才真正计算,一次 next 推进一格
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!(iter.next(), None); // None 表示迭代结束
assert_eq!(calls.get(), 3);
}

负责调用 next() 的角色叫消费者collectsumcountanyfor_each,以及 for 循环(本质上就是反复调 next() 直到 None)。记住判别法:适配器返回迭代器,消费者返回别的(集合、数字、Optionbool)。一条管道可以串任意多个适配器,但最终只能由一个消费者收口。

常用惰性适配器速览

日常管道最常用的适配器一次看全,仍用任务列表做输入:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#[derive(Debug)]
struct Task {
title: String,
state: &'static str, // "todo" | "doing" | "done"
labels: Vec<String>,
}

fn main() {
let tasks = vec![
Task { title: "修复登录".to_string(), state: "doing", labels: vec!["bug".to_string()] },
Task { title: "写发布说明".to_string(), state: "todo", labels: vec!["docs".to_string(), "release".to_string()] },
Task { title: "升级依赖".to_string(), state: "doing", labels: vec!["deps".to_string()] },
];

// enumerate:带序号
let numbered: Vec<String> = tasks
.iter()
.enumerate()
.map(|(i, t)| format!("{}. {}", i + 1, t.title))
.collect();
assert_eq!(numbered[0], "1. 修复登录");

// filter_map:过滤 + 转换一步完成,直接处理 Option
let doing_titles: Vec<String> = tasks
.iter()
.filter_map(|t| (t.state == "doing").then(|| t.title.clone()))
.collect();
assert_eq!(doing_titles, vec!["修复登录", "升级依赖"]);

// flat_map:把每个元素的子集合拍平成一条流
let all_labels: Vec<&str> = tasks
.iter()
.flat_map(|t| t.labels.iter().map(String::as_str))
.collect();
assert_eq!(all_labels, vec!["bug", "docs", "release", "deps"]);

// zip:两个迭代器对齐,短的结束即停(静默截断!)
let owners = ["小赵", "小钱"];
let pairs: Vec<(&str, &str)> = tasks
.iter()
.map(|t| t.title.as_str())
.zip(owners)
.collect();
assert_eq!(pairs.len(), 2); // owners 只有 2 个,第 3 个任务被丢弃

// take / skip:分页的基本件
let page2: Vec<&str> = tasks.iter().skip(1).take(1).map(|t| t.title.as_str()).collect();
assert_eq!(page2, vec!["写发布说明"]);

// chain:把两条迭代器接成一条
let extra = vec!["回顾会议"];
let all: Vec<&str> = tasks
.iter()
.map(|t| t.title.as_str())
.chain(extra.iter().copied())
.collect();
assert_eq!(all.len(), 4);

// scan:带内部状态的转换,每一步都可以决定输出什么
let cumulative: Vec<usize> = tasks
.iter()
.map(|t| t.labels.len())
.scan(0usize, |acc, n| {
*acc += n;
Some(*acc)
})
.collect();
assert_eq!(cumulative, vec![1, 3, 4]);
}

其他值得知道的适配器:cloned()/copied()(把 &T 流转成 T 流)、peekable()(允许偷看下一项)、rev()(反向,要求双端迭代器)、step_by(n)(隔项取样)、fuse()(保证 None 之后永远 None)、by_ref()(借用迭代器以便分段消费)。遇到时查标准库文档即可,模式与上面相同。

常用消费者速览

消费者决定管道算什么何时停

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 main() {
let durations = vec![30u32, 45, 15, 60, 20]; // 任务耗时(分钟)

// sum / count / any / all:聚合与判定
let total: u32 = durations.iter().sum();
assert_eq!(total, 170);
assert_eq!(durations.iter().count(), 5);
assert!(durations.iter().any(|d| *d > 50));
assert!(durations.iter().all(|d| *d >= 10));

// find / position:短路,命中即停,不再遍历后续元素
let first_long = durations.iter().find(|d| **d >= 45);
assert_eq!(first_long, Some(&45));
assert_eq!(durations.iter().position(|d| *d == 60), Some(3));

// fold:最通用的消费者,sum/max/count 都是它的特例
let max = durations.iter().fold(0u32, |acc, d| acc.max(*d));
assert_eq!(max, 60);

// 语义化的特例:max / min / last,对空迭代器返回 None
assert_eq!(durations.iter().max(), Some(&60));
assert_eq!(durations.iter().last(), Some(&20));

// partition:一次遍历按条件分成两组
let (quick, slow): (Vec<u32>, Vec<u32>) =
durations.iter().copied().partition(|d| *d <= 30);
assert_eq!(quick, vec![30, 15, 20]);
assert_eq!(slow, vec![45, 60]);
}

两个使用要点:find/max/last 这类返回 Option 的消费者天然处理空迭代器,不需要先判空;any/all/find/position 是短路的,放在链条末端不会浪费计算。

collect:类型推断与进阶收集

collect 是链条上唯一必然分配的节点,它的目标类型只要实现了 FromIterator 就行——远不止 Vec

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

fn main() {
let numbers: Vec<i32> = vec![1, 2, 3, 4, 5, 6];

// 目标类型写不清时编译失败,用 turbofish 或类型标注
let doubled = numbers.iter().map(|n| n * 2).collect::<Vec<i32>>();
assert_eq!(doubled, vec![2, 4, 6, 8, 10, 12]);

// 收集到 HashSet:自动去重
let unique: HashSet<i32> = vec![1, 2, 2, 3].into_iter().collect();
assert_eq!(unique.len(), 3);

// 收集到 HashMap:源是 (K, V) 元组流
let scores: HashMap<&str, u32> = [("登录", 30), ("支付", 45)].into_iter().collect();
assert_eq!(scores.get("支付"), Some(&45));

// 收集到 Result<Vec<_>, _>:任一元素失败则整体失败
let parsed: Result<Vec<u32>, _> = ["1", "2", "3"].iter().map(|s| s.parse::<u32>()).collect();
assert_eq!(parsed.unwrap(), vec![1, 2, 3]);
let bad: Result<Vec<u32>, _> = ["1", "x"].iter().map(|s| s.parse::<u32>()).collect();
assert!(bad.is_err());
}

借用版管道:返回值活不过输入

任务列表最常见的查询是"取所有进行中任务的标题"。写出带显式生命周期标注的版本,让借用关系一目了然:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TaskId(u64);

impl TaskId {
fn new(value: u64) -> Result<Self, String> {
if value == 0 {
Err("TaskId 不能为 0".to_string())
} else {
Ok(TaskId(value))
}
}
}

#[derive(Debug, PartialEq)]
enum TaskState {
Todo,
InProgress { started_at: u64 },
Done { finished_at: u64 },
Cancelled { reason: String },
}

#[derive(Debug, PartialEq)]
struct Task {
id: TaskId,
title: String,
state: TaskState,
labels: Vec<String>,
}

/// 借用版:返回的 &str 只在输入 Vec 存活期间有效。
/// 显式生命周期标注让这层约束一目了然。
fn active_titles<'a>(tasks: &'a [Task]) -> Vec<&'a str> {
tasks
.iter()
.filter(|task| matches!(task.state, TaskState::InProgress { .. }))
.map(|task| task.title.as_str())
.collect()
}

/// 拥有版:结果需要超过输入生命周期(缓存、跨线程、跨请求)时使用。
fn active_titles_owned(tasks: &[Task]) -> Vec<String> {
tasks
.iter()
.filter(|task| matches!(task.state, TaskState::InProgress { .. }))
.map(|task| task.title.clone())
.collect()
}

fn main() {
let tasks = vec![
Task {
id: TaskId::new(1).expect("1 is a valid id"),
title: "修复登录超时".to_string(),
state: TaskState::InProgress { started_at: 100 },
labels: vec!["bug".to_string()],
},
Task {
id: TaskId::new(2).expect("2 is a valid id"),
title: "整理发布说明".to_string(),
state: TaskState::Todo,
labels: Vec::new(),
},
Task {
id: TaskId::new(3).expect("3 is a valid id"),
title: "升级依赖".to_string(),
state: TaskState::InProgress { started_at: 200 },
labels: vec!["deps".to_string()],
},
];

let borrowed = active_titles(&tasks);
assert_eq!(borrowed, vec!["修复登录超时", "升级依赖"]);

let owned = active_titles_owned(&tasks);
assert_eq!(owned, vec!["修复登录超时".to_string(), "升级依赖".to_string()]);

// 拥有版可以在输入销毁后继续使用
drop(tasks);
assert_eq!(owned.len(), 2);
}

这里 <'a> 的含义是描述关系,不是延长寿命:返回的每个 &str 都来自某个 task.title,所以它们的最长寿命就是 tasks 的寿命。如果试图让借用结果活过输入,编译器会用 E0505 拒绝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
无法编译:借用结果在输入集合被销毁后继续使用,触发 E0505。

fn main() {
let titles = vec!["登录".to_string(), "支付".to_string()];
let borrowed: Vec<&str> = titles.iter().map(|t| t.as_str()).collect();
drop(titles); // 输入集合在这里被销毁
assert_eq!(borrowed.len(), 2); // ERROR: cannot move out of `titles`
// because it is borrowed (E0505)
}

修正(可运行):需要结果超过输入时,收集 String 而不是 &str:

fn main() {
let titles = vec!["登录".to_string(), "支付".to_string()];
let borrowed: Vec<String> = titles.iter().cloned().collect();
drop(titles); // 输入已销毁,但 owned 数据与其无关
assert_eq!(borrowed.len(), 2);
assert_eq!(borrowed[0], "登录");
}

概念讲解:闭包捕获与 Fn/FnMut/FnOnce

管道里每个 |task| ... 都是闭包。闭包对环境的使用方式决定它实现哪个 trait,编译器自动推断;写 API 时才需要显式约束。捕获策略默认尽量借用:能用 & 就不用 &mut,能用 &mut 就不转移所有权;move 关键字用于强制按值捕获(下文 FnOnce 示例)。三者是包含关系:FnFnMutFnOnce(实现 Fn 的也能当 FnMut/FnOnce 用):

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
30
31
32
33
34
35
36
37
#[derive(Debug)]
struct Task {
title: String,
}

fn main() {
let tasks = vec![
Task { title: "写发布说明".to_string() },
Task { title: "升级依赖".to_string() },
];

// Fn:只读使用环境,可重复调用(这里被 map 调用多次)
let quota = 10;
let titles: Vec<String> = tasks
.iter()
.map(|task| format!("[{quota}] {}", task.title))
.collect();
assert_eq!(titles, vec!["[10] 写发布说明", "[10] 升级依赖"]);

// FnMut:闭包修改自己的捕获状态,仍可重复调用
let mut next_id = 100;
let ids: Vec<u64> = tasks
.iter()
.map(|_| {
next_id += 1;
next_id
})
.collect();
assert_eq!(ids, vec![101, 102]);
assert_eq!(next_id, 102);

// FnOnce:消耗捕获的值,只能调用一次
let banner = String::from("== backlog ==");
let header = move || banner; // move 把 banner 的所有权移进闭包
let text = header();
assert_eq!(text, "== backlog ==");
}

日常判别法:闭包体内只读环境 → Fn修改捕获变量 → FnMut消耗move 后只用一次、按值使用)→ FnOnce。接收闭包的函数签名从最宽松的开始要求:能声明 impl Fn(...) 就不要声明 impl FnOnce(...),让调用方保留最大灵活性。

边界与失败场景

  • 迭代器被中途消费iter() 产生的迭代器是 Iteratorcollect 之后再 collect 会编译失败(Iterator 已被 move);by_ref() 可以保留迭代器供后续使用。
  • for t in tasks 悄悄消耗集合for 循环等价于 into_iter(),循环结束后 tasks 不可再用;之后还要用集合就写 for t in &tasks
  • zip 静默截断:两个迭代器长度不同以短的为准,多余元素被丢弃且无警告;需要配对完整性时先检查长度。
  • 适配器写成独立语句tasks.iter().map(...) 单独一行只是构造迭代器,一行都不会执行;编译器用 unused_must_use 警告提醒,别靠 #[allow] 压掉它。

为什么可行:零成本不是宣传语

filter/map 返回的是新的迭代器类型,链条上的每一步都携带上游类型参数,编译器展开后没有虚调用和中间集合;collect 是链条上唯一必然分配的节点,因此返回值生命周期天然绑定输入。闭包同理:它是编译器生成的匿名结构体,捕获的环境就是字段,Fn/FnMut/FnOnce 分别对应以 &self&mut selfself 调用,单态化后与手写函数同价。迭代器管道和手写的 for 循环生成几乎相同的机器码——你选择管道不是为了快,而是为了把"转换的每一步"写成可审查的具名结构。

常见误区

  • 迭代器链无限延长filter().map().filter().flat_map().scan() 一条链 10 步不如拆成具名函数。可读性优先于"函数式纯度"。
  • 忘记 collect 的类型let v = iter.collect() 无法推断目标类型时编译失败;写 collect::<Vec<_>>() 最省事。
  • 该用 into_iter() 时用 iter().cloned():能消耗集合就直接产出 T,省一层克隆;反过来只想看一眼就用 iter(),别为了顺手把集合 move 掉。
  • 为避免 clone 写生命周期体操title.clone() 通常比让 &str 贯穿五层签名便宜(指维护成本)。先测,再优化。

自测

  1. fn active_titles<'a>(tasks: &'a [Task]) -> Vec<&'a str>'a 是在声明什么关系?它能让返回值活得更久吗?
    答的方向:声明"返回值借用自输入"的来源关系;不能延长寿命,寿命由调用方实际数据决定,标注只是让编译器验证。
  2. for task in &tasksfor task in tasks 的行为差异是什么?后者循环结束后 tasks 还能用吗?
    答的方向:前者等价于 iter(),只借用;后者等价于 into_iter(),集合被 move,循环结束后不可再用。
  3. 一个闭包体内修改了捕获的计数器变量,它实现 FnFnMut 还是 FnOnce?如果它把捕获的 String 按值返回呢?
    答的方向:修改捕获 → FnMut;消耗捕获值 → FnOnce