课程概览 · 第 12 章
上一篇:集合:从访问模式选择 Vec、Map、Set 与队列
下一篇:错误处理:区分可恢复失败与程序缺陷
第 11 章讨论数据放在哪里,本章讨论如何转换数据。迭代器描述逐项读取、过滤与映射,闭包提供转换逻辑及其需要的环境。读一条管道时,依次确认入口的所有权、计算的驱动点和结果是否借用输入;这比记住许多方法名更重要。
图:分开判断计算何时发生、输入归谁、闭包如何使用环境。前置知识与学习目标
需要理解借用、生命周期与集合基本操作。学完本章应能:
- 预测
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 | 数值;通常需要遍历到结束 |
find | Option<Self::Item>;命中即停 |
max / min / last | Option<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"); }
|
自测
iter().find 与 into_iter().find 的返回类型为什么不同?filter 的参数又是什么?- 惰性计数例构造 map 后、调用三次 next 后,计数分别是多少?
- 无限 repeat 配
any 能否保证结束?collect 是否必然分配? - 为什么 move 闭包能重复调用,而没有 move 的 take_title 只能调用一次?
- 返回借用标题与拥有标题各有什么边界?
参考答案
- 它们的 Item 分别为引用和值;find 返回 Option
- ,filter 接收 &Item。
- 分别为 0、3;本例每次 next 映射一项。
- any 需要命中才短路,不保证结束;分配由目标 FromIterator 与转换逻辑决定。
- 调用能力取决于闭包体:只借用捕获值可重复,移出 String 会消费闭包。
- 借用结果不能活过输入,拥有结果可独立保存,但取得拥有值可能需要克隆或消费输入。