课程概览 · 第 12 章
上一篇:集合:从访问模式选择 Vec、Map、Set 与队列
下一篇:错误处理:区分可恢复失败与程序缺陷
第 11 章解决了"数据放哪个容器",本章解决"数据怎么转换":当一条查询要穿过过滤、映射、分组多个步骤,当转换结果要脱离输入长期保存时,手写 for 循环加中间 Vec 开始显得笨重。Rust 的答案是两个配合使用的工具:迭代器回答"转换何时执行",闭包回答"环境怎么捕获"。本章仍在任务队列/CLI 业务域里,把迭代器管道从入口选择讲到生命周期边界,把闭包从捕获方式讲到 Fn/FnMut/FnOnce。
图:转换管道并不立即执行;先找消费者,再检查输入的所有权与闭包对外部环境的捕获权限。 学习目标与默认选择 学完本章你应当能够:
区分 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<_>>() 的类型推断边界,并能把结果收集到 HashMap、HashSet、Result<Vec<_>, _>; 用 Fn/FnMut/FnOnce 描述闭包对环境的使用方式。 默认选择 :迭代器和闭包是日常默认,不是优化技巧——优先用管道表达转换;只有当链条长到影响可读性时才拆成具名函数或退回 for 循环。
概念讲解:迭代器管道与生命周期 for 循环能做的,迭代器管道都能做,而且每一步都可命名、可组合。迭代器真正的学习成本不在语法,而在三件事:入口选哪个 (借用还是消耗)、计算何时发生 (惰性)、结果能活多久 (生命周期绑定输入)。本节按这个顺序展开。
三种迭代入口:iter、iter_mut、into_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 }, ]; let titles : Vec <&str > = tasks.iter ().map (|t| t.title.as_str ()).collect (); assert_eq! (titles, vec! ["写发布说明" , "升级依赖" ]); 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.len (), 1 ); assert_eq! (owned[0 ].title, "升级依赖" ); }
注意数组与 Vec 的差异:vec.into_iter() 一直按值产出 T;而数组在 Rust 2021 之前 array.into_iter() 会自动退化为引用迭代(产出 &T),2021 起才统一为按值产出。读旧代码时这是一个常见的困惑点。
惰性由 next() 驱动:适配器与消费者 所有迭代器实现同一个 trait,核心只有一个方法:
1 2 3 4 5 6 trait Iterator { type Item ; fn next (&mut self ) -> Option <Self ::Item>; }
map、filter 这类适配器 返回的是包裹上游的新迭代器,本身一次计算都不做;只有 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 ]; 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! (iter.next (), None ); assert_eq! (calls.get (), 3 ); }
负责调用 next() 的角色叫消费者 :collect、sum、count、any、for_each,以及 for 循环(本质上就是反复调 next() 直到 None)。记住判别法:适配器返回迭代器,消费者返回别的 (集合、数字、Option、bool)。一条管道可以串任意多个适配器,但最终只能由一个消费者收口。
常用惰性适配器速览 日常管道最常用的适配器一次看全,仍用任务列表做输入:
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 , 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 ()] }, ]; let numbered : Vec <String > = tasks .iter () .enumerate () .map (|(i, t)| format! ("{}. {}" , i + 1 , t.title)) .collect (); assert_eq! (numbered[0 ], "1. 修复登录" ); let doing_titles : Vec <String > = tasks .iter () .filter_map (|t| (t.state == "doing" ).then (|| t.title.clone ())) .collect (); assert_eq! (doing_titles, vec! ["修复登录" , "升级依赖" ]); 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" ]); let owners = ["小赵" , "小钱" ]; let pairs : Vec <(&str , &str )> = tasks .iter () .map (|t| t.title.as_str ()) .zip (owners) .collect (); assert_eq! (pairs.len (), 2 ); let page2 : Vec <&str > = tasks.iter ().skip (1 ).take (1 ).map (|t| t.title.as_str ()).collect (); assert_eq! (page2, vec! ["写发布说明" ]); 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 ); 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 ]; 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 )); 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 )); let max = durations.iter ().fold (0u32 , |acc, d| acc.max (*d)); assert_eq! (max, 60 ); assert_eq! (durations.iter ().max (), Some (&60 )); assert_eq! (durations.iter ().last (), Some (&20 )); 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 ]; let doubled = numbers.iter ().map (|n| n * 2 ).collect::<Vec <i32 >>(); assert_eq! (doubled, vec! [2 , 4 , 6 , 8 , 10 , 12 ]); let unique : HashSet<i32 > = vec! [1 , 2 , 2 , 3 ].into_iter ().collect (); assert_eq! (unique.len (), 3 ); let scores : HashMap<&str , u32 > = [("登录" , 30 ), ("支付" , 45 )].into_iter ().collect (); assert_eq! (scores.get ("支付" ), Some (&45 )); 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 >, }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 示例)。三者是包含关系:Fn ⊂ FnMut ⊂ FnOnce(实现 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 () }, ]; let quota = 10 ; let titles : Vec <String > = tasks .iter () .map (|task| format! ("[{quota}] {}" , task.title)) .collect (); assert_eq! (titles, vec! ["[10] 写发布说明" , "[10] 升级依赖" ]); 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 ); let banner = String ::from ("== backlog ==" ); let header = move || banner; let text = header (); assert_eq! (text, "== backlog ==" ); }
日常判别法:闭包体内只读 环境 → Fn;修改 捕获变量 → FnMut;消耗 (move 后只用一次、按值使用)→ FnOnce。接收闭包的函数签名从最宽松的开始要求:能声明 impl Fn(...) 就不要声明 impl FnOnce(...),让调用方保留最大灵活性。
边界与失败场景 迭代器被中途消费 :iter() 产生的迭代器是 Iterator,collect 之后再 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 self、self 调用,单态化后与手写函数同价。迭代器管道和手写的 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 贯穿五层签名便宜(指维护成本)。先测,再优化。自测 fn active_titles<'a>(tasks: &'a [Task]) -> Vec<&'a str> 里 'a 是在声明什么关系?它能让返回值活得更久吗?答的方向 :声明"返回值借用自输入"的来源关系;不能延长寿命,寿命由调用方实际数据决定,标注只是让编译器验证。for task in &tasks 和 for task in tasks 的行为差异是什么?后者循环结束后 tasks 还能用吗?答的方向 :前者等价于 iter(),只借用;后者等价于 into_iter(),集合被 move,循环结束后不可再用。一个闭包体内修改了捕获的计数器变量,它实现 Fn、FnMut 还是 FnOnce?如果它把捕获的 String 按值返回呢?答的方向 :修改捕获 → FnMut;消耗捕获值 → FnOnce。