如果你从 Python、JavaScript 或 Java 转到 Rust,第一次认真写迭代器链时,往往会同时撞上两个看起来互相矛盾的问题。
一个问题是:我只是想遍历一下,怎么原来的集合就不能用了?
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
let long_names = names.into_iter().filter(|name| name.len() >= 5);
println!("{names:?}");
println!("{}", long_names.count());
}这段代码过不了编译。into_iter() 接收的是 self,也就是把 names 整个拿走。它产生的迭代器持有向量中的元素,names 这个变量自然不能继续使用。编译器给你的核心信息会是 borrow of moved value: names。它不是嫌你遍历姿势不够优雅,而是在追问一个很具体的问题:向量里的 String 现在到底归谁?
另一个问题刚好相反:我明明写了处理逻辑,怎么它完全没执行?
fn main() {
let numbers = [1, 2, 3];
numbers.iter().map(|number| {
println!("正在处理 {number}");
number * 10
});
println!("程序结束");
}即使暂时不看编译器关于“未使用的惰性迭代器”的警告,运行时也只会看到:
程序结束前一个问题和所有权有关,后一个问题和惰性有关。把这两件事放到一起,Rust 迭代器就不再是一串需要背诵的方法名了。你可以先把它理解成一份“按需取下一项”的工作计划:map、filter 这类方法只是在改计划,collect、sum、for 这类操作才会真的沿着计划,一项一项地取数据。
这套设计一开始确实会让人不舒服。闭包参数有时是 T,有时是 &T,到了 filter 里又像多了一层引用;collect() 还经常反问你到底想收集成什么类型。别急着把这些都归咎于“语法怪”。大部分困惑都能沿着两条线查清楚:当前迭代器的 Item 是什么,以及谁拥有正在被迭代的数据。
Rust 的迭代器核心很小。Iterator trait 要求一个关联类型 Item,再要求实现一个 next 方法。把细节压缩以后,可以看成这样:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}每调用一次 next(),迭代器就尝试交出下一项。还有数据时返回 Some(item),当前遍历结束时返回 None。next 需要 &mut self,因为迭代器必须记住自己走到哪里了。
fn main() {
let words = ["rust", "iterator", "next"];
let mut iter = words.iter();
assert_eq!(iter.next(), Some(&"rust"));
assert_eq!(iter.next(), Some(&"iterator"));
assert_eq!(iter
这里 words.iter() 的 Item 是 &str 的引用,也就是 &&str。数组里每个元素本身是 &str,iter() 又借用了元素,所以 next() 返回 Option<&&str>。Some(&"rust") 正好对应这个类型。你以后遇到闭包参数对不上,先写下 Item = ?,通常比盯着整条链猜要快得多。
Iterator 提供的大多数方法,都建立在“不断调用 next”这件事上。map 包装原迭代器,等别人向它要下一项时,它再向上游要一项并转换;filter 可能向上游多要几项,直到找到符合条件的那一项;sum 则一路调用 next,直到遇到 None,最后交出总和。
所以“迭代器被消费”不等于“底层集合一定被销毁”。被消费的首先是迭代器这个状态机。底层数据会不会被移动,要看你从什么入口创建它。
排查迭代器类型问题时,先问三个问题:这条链当前的 Item 是什么,下一步闭包接收的是 Item 还是 &Item,创建迭代器时数据被借用还是被移动。编译器给出的长类型只是这三件事展开后的结果。
对 Vec<T>、切片和许多集合来说,最常见的入口是 iter()、iter_mut() 和 into_iter()。它们看起来只差几个字符,做出的所有权决定却完全不同。

如果这三种入口仍然容易在脑中打架,可以在下面的实验里切换入口,观察元素类型和原集合可用性怎样一起变化。先凭直觉选一次,再对照结果找出所有权发生在哪一步。
iter():借来看看fn main() {
let names = vec![String::from("Lin"), String::from("Barbara")];
let total_bytes: usize = names.iter().map(|name| name.len()).sum();
println!(
总字节数:10
原数据:["Lin", "Barbara"]name 的类型是 &String。调用 len() 只需要共享借用,所以整条链没有必要复制或移动任何字符串。sum() 会消费迭代器,但这个迭代器只是持有对 names 的借用;当 sum() 结束,借用也结束,names 仍然在。
如果你最后确实需要拥有元素,可以在合适的位置使用 copied() 或 cloned()。copied() 只适用于实现了 Copy 的值,cloned() 会调用 Clone。对昂贵的值,先筛选再克隆通常更合理:
fn main() {
let names = vec![
String::from("Li"),
String::from("Margaret"),
String::from("Ken"),
];
let long_names: Vec<String> = names
.iter()
.filter(
这段代码只克隆真正通过筛选的字符串。如果一上来就 .iter().cloned().filter(...),被淘汰的元素也会先做一次无用的克隆。
iter_mut():借来改,但借用期间别从旁边伸手fn main() {
let mut prices = vec![100, 250, 80];
for price in prices.iter_mut() {
*price += 20;
}
assert_eq!(prices, [120, 270, 100]);
}price 是 &mut i32。你修改的是向量里的原元素,不是在修改副本。这里的限制也很直接:可变迭代器还活着并且后面还会使用时,不能同时读写原集合。
fn main() {
let mut values = vec![1, 2, 3];
let mut iter = values.iter_mut();
let first = iter.next().unwrap();
// values.push(4); // 不能在可变借用仍被使用时再次可变借用 values
*first *= 10;
iter.for_each(
编译器在这里像一个严格的现场管理员:你已经派出一个可以修改向量元素的迭代器,它就不会允许另一个操作同时改变向量布局。push 可能触发重新分配;如果旧引用还指着原来的内存,后果就不是“值算错一点”,而是引用失效。
into_iter():把数据交出去fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
let decorated: Vec<String> = names
.into_iter()
.map(|name| format!("工程师:{name}"))
.
此时 Item = String。闭包拿到每个字符串的所有权,可以把它直接放进别的结构,也可以消费它来构造新值,不需要克隆。代价就是原来的 Vec<String> 已经交给 into_iter()。
对实现了 Copy 的数组元素,现象容易让人误判:
fn main() {
let numbers = [1, 2, 3];
let doubled: Vec<_> = numbers.into_iter().map(|n| n * 2).collect();
println!("{numbers:?}");
println!("{doubled:?}");
它能继续打印 numbers,不是因为 into_iter() 变成了借用,而是整个 [i32; 3] 实现了 Copy,按值传入时复制了一份。如果把元素换成 String,数组就会被移动。判断所有权时不要只看“代码还能不能用”,还要看类型有没有 Copy。
for 的三种常用写法fn main() {
let mut names = vec![String::from("ada"), String::from("grace")];
for name in &names {
println!("只读:{name}");
}
for name in &mut names {
name.make_ascii_uppercase();
}
for name in &names 与使用只读迭代入口的效果一致,元素是引用;for name in &mut names 产生可变引用;for name in names 按值迭代并消费向量。最后一个循环之后不能再用 names。
map、filter、take 这些方法返回的还是迭代器。它们只组装新的状态机,不会主动把上游走一遍。这就是惰性。
fn main() {
let numbers = [1, 2, 3, 4];
let plan = numbers
.iter()
.map(|number| {
println!("乘十:{number}");
number * 10
})
.filter(|number
计划已经建好
乘十:1
判断:10
乘十:2
判断:20
乘十:3
判断:30
乘十:4
判断:40
结果:[30, 40]输出顺序揭示了一个很重要的事实:它不是先把所有数字 map 完,再把中间结果整体交给 filter。消费者每要一项,数据就在链上向下走一格。filter 丢掉 10 后,会继续向上游要,直到能交出 30。

下面的追踪器会把每次拉取在 map、filter 和消费者之间的移动顺序展开。调整数据和链条后,你会更直观地看到“计划已经建好”与“工作真正执行”是两个时刻。
这里可以把方法粗略分成两类。
迭代器适配器接收一个迭代器,返回另一个迭代器,例如 map、filter、filter_map、flat_map、scan、take、skip 和 inspect。它们通常是惰性的。
消费者会推进迭代器并产生最终结果或副作用,例如 collect、sum、count、fold、reduce、any、all、find 和 for_each。for 循环也会不断推进迭代器。
“消费者”这个叫法很方便,但要留意方法签名。有些消费者按值取得 self,迭代器变量之后不能再用;any、all、find、try_fold 等方法接收 &mut self,短路之后,迭代器里未访问的部分仍可能继续使用。不要只凭分类猜所有权,签名才是最后答案。
下面这类代码很常见:
fn main() {
let mut output = Vec::new();
[1, 2, 3].iter().map(|n| output.push(n * 10));
assert!(output.is_empty());
}map 返回的新迭代器没有被消费,闭包一次都没调用。编译器通常还会发出 unused_must_use 警告,提醒你“迭代器是惰性的,不消费就什么也不做”。如果目的就是副作用,直接写 for 往往最清楚:
fn main() {
let mut output = Vec::new();
for n in [1, 2, 3] {
output.push(n * 10);
}
assert_eq!(output, [10, 20, 30]);
}也可以用 for_each,但别为了“看起来函数式”硬把直白的控制流塞进闭包。需要 break、continue、多个可变状态或复杂错误处理时,for 往往更容易读。
适配器的数量很多,不需要一次背完。更实用的办法是按“每个输入会产生多少输出”来理解。

map:一项进,一项出map 对每一项调用闭包,并把闭包返回值当成新迭代器的 Item。
fn main() {
let lengths: Vec<usize> = ["rust", "borrow", "iterator"]
.into_iter()
.map(str::len)
.collect();
assert_eq!(lengths, [4, 6, 8]);
}map 可以改变类型。输入是 &str,输出是 usize。如果你的闭包只是做日志、修改外部变量,却返回一个没人需要的值,通常应该停一下:你需要的也许是 for 或 for_each,并不是 map。
filter:要么保留原项,要么丢掉filter 不改变元素类型,只决定原来的项是否继续向下游走。它把 &Item 传给判断闭包,因为做判断不该夺走这一项,符合条件时还得把原项交出去。
fn main() {
let positives: Vec<i32> = [-2, 0, 3, 7]
.into_iter()
.filter(|number| *number > 0)
.collect();
assert_eq!(positives, [3, 7]);
这里上游的 Item = i32,所以 filter 的闭包参数是 &i32,要写 *number > 0。如果上游来自 .iter(),Item 已经是 &i32,filter 看到的就会是 &&i32。方法调用经常能靠自动解引用工作,但模式匹配时你可能会看到这样的写法:
fn main() {
let numbers = [1, 2, 3, 4];
let evens: Vec<i32> = numbers
.iter()
.filter(|&&number| number % 2 == 0)
.copied()
.collect
|&&number| 不是仪式感很强的 Rust 符号,它只是把 &&i32 这个参数解构成 i32。觉得绕时可以拆开写类型,或者在筛选后再 copied()。
filter_map:零项或一项,还能顺手改类型解析外部输入时,你经常既要转换,又要扔掉失败项。先 map 成 Result,再 filter,再 unwrap 能做,但链会显得绕。filter_map 让闭包直接返回 Option<B>:Some(value) 就向下游产生一项,None 就跳过。
fn main() {
let raw = ["42", " 7 ", "oops", "-3"];
let parsed: Vec<i32> = raw
.into_iter()
.filter_map(|text| text.trim().parse::<i32>().
这段代码明确选择了忽略坏数据。如果失败不能静默丢掉,就不要用 .ok() 抹掉错误。后面会看到如何把整条链收集成 Result,让第一个错误返回给调用者。
flat_map:一项可以展开成多项map 的闭包每次返回一个值;如果这个值本身也能转成迭代器,flat_map 会把它展开并接在同一条输出流上。
fn main() {
let lines = ["rust is strict", "iterators are lazy"];
let words: Vec<&str> = lines
.into_iter()
.flat_map(|line| line.split_whitespace())
.collect();
assert_eq!(
words,
[
如果写成 .map(|line| line.split_whitespace()),得到的是“每一行对应一个单词迭代器”的迭代器。flat_map 把这一层摊平。它适合一对多转换;如果你已经有一个 Iterator<Item = IntoIterator>,也可以用 flatten()。
别把 flat_map 当成消灭所有嵌套的万能按钮。它会隐藏一层循环,闭包再做很多事时,阅读者很难快速看出每个输入到底会产生多少项。逻辑复杂时,拆成命名函数或显式循环更友好。
scan:让映射过程记住状态scan 接收一个初始状态。每处理一项,闭包会拿到 &mut State 和当前 Item,然后返回 Option<Output>。它很适合前缀和、简单协议解析、连续编号这类“下一项依赖前面状态”的任务。
fn main() {
let running_totals: Vec<i32> = [2, 3, 5, 7]
.into_iter()
.scan(0, |total, number| {
*total += number;
Some(*total)
})
.collect
返回 None 时,当前消费者看到遍历结束,可以用它表达“达到条件后停止产生数据”:
fn main() {
let within_limit: Vec<i32> = [2, 3, 5, 7, 11]
.into_iter()
.scan(0, |total, number| {
let next = *total + number;
if next >
状态只属于这个 Scan 迭代器。它没有修改原数组。还要注意,Rust 的一般 Iterator 约定允许某些特殊迭代器在返回 None 后再次返回 Some;普通消费者会在第一次 None 时停止。如果你自己手动反复调用 next,又要求 None 之后永远结束,可以显式使用 fuse()。
skip 和 take:控制窗口skip(n) 丢掉前 n 项,之后继续产生;take(n) 最多产生前 n 项。它们组合起来很适合做简单分页或从无限序列截取有限部分。
fn main() {
let page: Vec<i32> = (1..)
.map(|n| n * n)
.skip(3)
.take(4)
.collect();
assert_eq!(page, [16, 25,
顺序不能随便换。先 skip(3).take(4) 是跳过三个后取四个;先 take(4).skip(3) 只会先限制到四项,再丢掉前三项,最后只剩一项。对昂贵的映射也要考虑位置:如果业务允许先截断再计算,尽早 take 能少做很多工作。
take 对无限迭代器尤其关键。(1..).collect::<Vec<_>>() 永远不会完成,而且会不断申请内存;(1..).take(100) 才是有限迭代器。短路消费者也能安全地在无限迭代器上工作,但前提是目标确实会在有限步内出现。
inspect:观察经过的项,不改变它inspect 把 &Item 交给闭包,然后原样放行 Item。它适合临时看一眼每一段链收到了什么。
fn main() {
let total: i32 = [1, 2, 3, 4]
.into_iter()
.filter(|n| n % 2 == 0)
.inspect(|n| println!("即将求和:{n}"))
.sum
即将求和:2
即将求和:4
总和:6inspect 仍然是惰性的,没有后面的 sum() 就不会打印。它也只观察真正走到这里的项:放在 filter 前后,看到的数据不同;下游如果 take(1) 或 find 提前停止,它不会观察剩余元素。
因此不要把必须执行的审计、计费或持久化操作偷偷藏在 inspect 里。代码重构一下链的顺序,就可能改变副作用次数。调试输出放这里很顺手,业务副作用最好放在明确的循环或专门的处理步骤中。
collect 的类型不是迭代器猜出来的collect() 的签名表达了一个很漂亮的分工:迭代器只负责产生 Item,目标类型负责说明如何从这些项构造自己。后半部分由 FromIterator trait 连接起来。
fn main() {
let doubled = [1, 2, 3]
.into_iter()
.map(|n| n * 2)
.collect::<Vec<_>>();
assert_eq!(doubled, [2, 4, 6]);
}Vec<T> 实现了从 T 迭代器构造向量的 FromIterator<T>,所以 collect::<Vec<_>>() 能工作。但同样的项还可以收集到别的结构:
use std::collections::{BTreeSet, HashMap};
fn main() {
let unique: BTreeSet<i32> = [3, 1, 3, 2].into_iter().collect();
let lengths: HashMap<&str,
HashMap<K, V> 需要迭代器产生 (K, V);String 可以从 char 迭代器构造;集合之间也能借助同一套机制转换。collect 自己并不知道你要 Vec、BTreeSet 还是 HashMap。
这也是类型推断最容易卡住的地方:
fn main() {
let values = (0..4).map(|n| n * 2).collect();
println!("{values:?}");
}编译器会要求类型注解,因为能装下 i32 的目标类型不止一个。它不是故意让你写“涡轮鱼”,只是现有上下文无法唯一决定 collect 的返回类型。两种常见修复都可以:
fn main() {
let left: Vec<i32> = (0..4).map(|n| n * 2).collect();
let right = (0..4).map(|n| n * 2
通常把类型写在最能表达意图的位置。变量本来就代表一个明确的数据结构时,用左侧注解很好读;链很长、错误离 collect 很近时,涡轮鱼更容易定位。

迭代器产生 Result<T, E> 时,可以直接收集成 Result<Vec<T>, E>。所有项都是 Ok,就得到 Ok(Vec<T>);遇到第一个 Err,收集立刻短路并返回错误。
fn parse_ports(raw: &[&str]) -> Result<Vec<u16>, std::num::ParseIntError> {
raw.iter()
.map(|text| text.parse::<u16>())
.collect::<Result<Vec
这与前面的 filter_map(Result::ok) 做的是不同选择。filter_map 忽略错误,收集成 Result 则保留第一个错误并停止。配置、金额、权限这类输入通常不能悄悄丢掉,应该让错误继续向上传递。
Iterator::try_collect() 也描述“可失败地收集”,但它目前仍是 nightly-only 的实验接口,普通 stable Rust 不能直接调用。stable 项目里,迭代器项是 Result 或 Option 时,直接用 collect::<Result<Vec<_>, _>>() 或 collect::<Option<Vec<_>>>() 就能得到常见的短路收集行为。不要为了一个方法名把整个项目切到 nightly。
FromIterator当自定义类型确实是一种“由一串项构造出来的容器或汇总结果”时,也可以实现 FromIterator。
#[derive(Debug, PartialEq)]
struct NonEmptyStrings {
values: Vec<String>,
skipped: usize,
}
impl FromIterator<String> for NonEmptyStrings {
fn from_iter<I: IntoIterator<Item = String>>(items: I) ->
这里 from_iter 的参数接受任何能转成 Iterator<Item = String> 的类型,而不是只接受某一种具体迭代器。这样实现之后,调用方不必知道内部用了 Vec,只要写目标类型就可以收集。
有些消费者不需要走完整条链。any 找到第一个满足条件的项就返回 true;all 找到第一个不满足条件的项就返回 false;find 返回第一个匹配项;try_fold 在闭包返回失败时立即结束。
any 与 allfn main() {
let has_large = [3, 8, 120, 7]
.into_iter()
.inspect(|n| println!("any 检查 {n}"))
.any(|n| n > 100);
assert!(has_large);
}any 检查 3
any 检查 8
any 检查 1207 没有被访问。把日志、计数等副作用放进短路链时,不能假设每项都会执行。

短路最容易误判的是“已经消费到哪里”和“还剩下什么”。你可以在下面改变命中条件,逐步查看消费者停止时,上游哪些项已经取出、哪些项仍留在迭代器中。
all 对空迭代器返回 true,any 对空迭代器返回 false。这不是边角怪规则:空集合里确实找不到反例来推翻“所有项都满足条件”,也找不到一个例子来证明“存在满足条件的项”。写校验逻辑时,先决定空输入是否允许;如果业务要求至少有一项,需要单独检查。
find 返回项本身fn main() {
let users = ["guest", "maintainer", "admin", "backup"];
let first_long = users.into_iter().find(|name| name.len() >= 8);
assert_eq!(first_long, Some("maintainer"));
}find 的判断闭包接收 &Item,成功后返回 Option<Item>。它交出的是原迭代器产生的项,不只是一个布尔值。如果需要“找到并转换”,可以进一步了解 find_map,但不要把所有操作压成一行,先保证条件和返回值读得清楚。
any、all 和 find 接收的是 &mut self。已经检查过的项被消耗,没检查的项还在。
fn main() {
let mut iter = [2, 4, 7, 8, 10].into_iter();
let found_odd = iter.any(|n| n % 2 != 0);
let remaining: Vec<_> = iter.
any 读到 7 后返回,7 本身已经被消费,剩下的是 8 和 10。这在流式解析中很有用,也很容易造成误会:如果后面还要完整数据,就不应该先在同一个迭代器上短路查询。
try_fold:累积过程中允许失败普通 fold 要求每一步都返回新的累积值。try_fold 允许闭包返回 Result、Option 等带“成功或提前退出”含义的类型。
fn main() {
let mut iter = [10_u8, 20, 250, 3].into_iter();
let total: Result<u8, &str> = iter.try_fold(0_u8, |sum, n| {
sum.checked_add
前两项把累积值变成 30;处理 250 时加法失败,try_fold 立即返回 Err。触发错误的 250 已经从迭代器中取出,后面的 3 还在。这个细节在“失败后重试”场景里很关键:继续迭代不是重新处理失败项,而是从它后面继续。
如果你的任务只想逐项执行可失败操作,不需要累积状态,可以使用 try_for_each;如果要保留成功值,收集成 Result<Vec<_>, E> 通常更直接。
短路只保证停止向上游继续取项,不会撤销已经发生的副作用。前面已经写入文件、发送数据或修改状态的操作不会自动回滚。需要事务语义时,要在迭代器之外设计提交与回滚边界。
fold 与 reduce 都在合并,但起点不同fold 接收一个显式初始值,然后按从左到右的顺序,把累积值和每一项交给闭包。
fn main() {
let summary = ["rust", "iterator", "ownership"]
.into_iter()
.fold(String::from("主题"), |mut text, word| {
text.push_str(" -> ");
text.push_str(word);
text
});
这里累积值是 String,迭代器项是 &str。两者可以不同。显式初始值还让空迭代器有自然结果:没有任何项时,fold 直接返回初始值。
reduce 不接收初始值,而是把第一项当成累积值,再从第二项开始合并。因此它要求累积值和 Item 是同一种类型,并返回 Option<Item>:空迭代器没有第一项,只能返回 None。
fn main() {
let max = [4, 9, 2, 7]
.into_iter()
.reduce(|best, n| if n > best { n } else { best });
let empty = std::iter::empty::<i32>().reduce
选择时可以直接问:有没有合理的初始值?累积结果是否可能和元素类型不同?需要明确初始值就用 fold;让第一项自然充当起点时,reduce 很合适。

还要正视运算顺序。fold 是从左向右组合。加法这类满足结合律的运算通常不容易看出差异,减法、除法、浮点累积和字符串拼接都会受到顺序影响。
fn main() {
let result = [10, 3, 2].into_iter().fold(0, |acc, n| acc - n);
assert_eq!(result, -15); // ((0 - 10) - 3) - 2
}不要因为方法名叫“折叠”就默认编译器可以随意重排。并行归约是另一类问题,需要运算满足相应性质,还要明确浮点误差是否可接受。
很多统计都有专用消费者,例如 sum、product、min、max。专用方法更准确地表达意图时就用它们。fold 的优势是通用,不是越通用就越值得优先。
for 循环背后是 IntoIterator 和 next写下:
for item in values {
println!("{item}");
}你没有显式调用 iter(),循环却知道怎么取元素,是因为 for 会先把 values 交给 IntoIterator::into_iter,取得真正的迭代器,再重复调用 next()。把错误处理、作用域等细节先放到一边,它的核心过程可以近似理解为:
let mut iterator = IntoIterator::into_iter(values);
loop {
match iterator.next() {
Some(item) => {
println!("{item}");
}
None => break,
}
}IntoIterator 和 Iterator 不是同一个 trait。前者回答“这个值怎样变成迭代器”,后者回答“已经得到迭代器后,怎样取下一项”。集合通常实现 IntoIterator;转换得到的迭代器实现 Iterator。

这也解释了为什么同一个集合能有三种 for 行为。以 Vec<T> 为例,可以把它们理解成分别对三种接收者实现了转换:
fn main() {
let mut values = vec![1, 2, 3];
for value in &values {
assert!(*value >= 1); // Item = &i32
}
for value in &mut values {
*value *= 10; // Item = &mut i32
}
对 API 设计来说,接收 IntoIterator 往往比接收具体的 Vec<T> 更灵活。调用方可以传数组、向量或已经构造好的迭代器。
use std::fmt::Display;
fn print_items<I>(items: I)
where
I: IntoIterator,
I::Item: Display,
{
for item in items {
println!("{item}");
}
}
fn main() {
函数体不关心输入原本是什么容器,只关心它能产生可显示的项。所有实现了 Iterator 的类型本身也能转成迭代器,所以最后那条 map 链可以直接传入。
不过,泛型边界里的所有权依旧要写清楚。I: IntoIterator<Item = String> 表示按值拿到字符串;如果你只想借用调用方的数据,通常还要设计生命周期和引用项类型。别为了接口“万能”堆出难读的约束。只在调用方确实需要多种输入时使用泛型。
next 的契约假设你需要一个从起点开始、每次增加固定步长、到上限为止的序列。与其先构造整个 Vec,不如让它按需产生数字。
#[derive(Debug)]
struct Stepper {
next_value: i32,
end: i32,
step: i32,
finished: bool,
}
impl Stepper {
fn new(start: i32, end: i32, step: i32) -> Self {
assert!

自定义迭代器真正需要守住的是每次 next() 前后状态怎样变化。下面的状态机实验可以逐次推进,观察返回值、下一候选值与结束标记何时更新。
实现里有几个值得慢慢看清的决定。
type Item = i32 决定每次成功调用交出什么。它不是固定只能用值;借用型迭代器也能产生引用,只是那会牵涉迭代器持有的数据和生命周期,复杂度更高。
next(&mut self) 先判断结束,再保存当前值,然后推进内部状态。返回当前值而不是推进后的值,能避免漏掉起点。状态更新顺序写反,是自定义迭代器最常见的 off-by-one 来源之一。
checked_add 防止接近 i32::MAX 时溢出。这里选择在无法继续时,把当前合法值返回,并把后续状态标记为结束。之后每次调用都返回 None。虽然一般 Iterator 允许个别实现从 None 恢复,但一个普通有限序列最好遵守“结束后一直结束”的直觉。
只实现 next 后,map、filter、take、collect 等默认方法就都能使用,for 也能直接遍历它。真正的价值不只是少写一个循环,而是你把“怎样产生下一项”封装成了一个可以和整个迭代器生态组合的类型。
复杂迭代器还可以实现 size_hint、DoubleEndedIterator、ExactSizeIterator 或 FusedIterator,但这些 trait 都是在承诺更多性质。比如 ExactSizeIterator 不是“我大概知道长度”,而是剩余长度必须准确。没有证明之前别急着实现,错误的优化承诺比不优化更麻烦。
刚开始学时,我们很容易只盯着集合有没有被移动,却忘了迭代器本身也是一个普通的 Rust 值。它有类型、有所有权,也会被方法调用移动。多数适配器的方法接收 self,因为新适配器需要把旧迭代器包在自己里面。
fn main() {
let iter = [1, 2, 3].into_iter();
let doubled = iter.map(|n| n * 2);
// println!("{}", iter.count()); // iter 已被 map 移动
assert_eq!(doubled.sum::<i32>(), 12);
}map 并没有立即读取数字,但它取得了 iter 的所有权。原因不难理解:将来 doubled.next() 被调用时,Map 必须能找到原来的迭代器,向它索要下一项。因此,旧迭代器已经成为新迭代器内部的一部分。
这解释了一个常见误区:“链是惰性的,所以原迭代器应该没变化。”惰性只表示还没有拉取元素,不表示适配器不发生所有权转移。数据处理还没开始,装配好的状态机已经有了新的主人。
消费者的签名也不完全相同。count、sum、collect、fold 和 reduce 通常按值接收 self,调用后迭代器变量不能继续用。next、nth、find、any、all、try_fold 等需要保留短路后的剩余状态,接收的是 &mut self。你不必死背这张表,但当“为什么这个迭代器还能用”或“为什么被移动了”出现时,应该去看接收者是 self 还是 &mut self。
by_ref 分段消费同一个迭代器有时你确实想把同一条输入流分段处理。例如,先把前两个单词当成文件头,再把其余单词当成正文。如果直接调用 take(2),take 会取得迭代器所有权,后面无法继续使用原变量。by_ref() 提供一个对迭代器的可变引用,让取得所有权的方法只拿走这层引用,原迭代器还留在当前作用域。
fn main() {
let mut words = "version 3 rust iterator is lazy"
.split_whitespace();
let header: Vec<_> = words.by_ref().take(2).collect();
let body: Vec<_> = words.collect();
assert_eq!(header, ["version"
这里 take(2) 消费的是 &mut words 这个迭代器适配器,而不是把 words 变量永久搬走。前两项依然从原迭代器中被取走,所以第二次 collect 只会拿到剩余内容。
by_ref 不会复制迭代器,也不会创建一个从头开始的新视图。它只是让一段调用链临时借用原迭代器。把它误解成“复制一个分支”,就会对结果产生错误预期。一个普通迭代器是一条向前推进的状态流,不会自动保存历史位置。
创建 iter() 或 iter_mut() 时,借用往往会持续到迭代器最后一次被使用。Rust 的非词法生命周期会尽量在最后一次使用后结束借用,而不是机械地等到大括号结束。即便如此,只要迭代器后面还要用,借用就仍然有效。
fn main() {
let mut values = vec![1, 2, 3];
let mut iter = values.iter_mut();
*iter.next().unwrap() *= 10;
*iter.next().unwrap() *= 10
如果在 values.push(4) 后面又调用一次 iter.next(),编译器就会拒绝 push,因为可变迭代器仍需保持对原向量的借用。这个判断看的是后续使用,不只是变量是否还在作用域里。
当借用关系太绕时,用显式作用域通常比和错误信息拉扯更省时间:
fn main() {
let mut values = vec![1, 2, 3];
{
let iter = values.iter_mut();
iter.for_each(|n| *n *= 10);
}
values.push(40);
大括号清楚地告诉读者:原地修改在这里完成,接下来可以重新操作向量。靠编译器推导最短生命周期能让代码更紧凑,显式边界则能让复杂逻辑更容易维护,两种写法都合理。
迭代器方法常接收 FnMut 闭包,闭包可以修改自己捕获的外部状态。下面这段代码统计真正通过筛选的次数:
fn main() {
let mut accepted = 0;
let values: Vec<i32> = [1, 2, 3, 4, 5]
.into_iter()
.filter(|n| {
let keep = n % 2
filter 的闭包在迭代器被消费时才执行,所以 accepted 的最终值要等 collect 完成后才确定。若你把链保存到变量中,又在消费之前读取 accepted,闭包持有的可变借用可能与读取冲突。
fn main() {
let mut seen = 0;
let plan = [1, 2, 3].into_iter().inspect(|_| seen += 1);
// println!("{seen}"); // plan 后面还要用,闭包仍持有对 seen 的可变借用
let count = plan.count();
assert_eq!(count,
编译器拦住的不是 inspect 这个方法,而是同一时间对 seen 的互斥访问。如果计数是业务结果,直接把它纳入 fold 的累积值通常更清楚;如果只是调试,及时消费链,不要让带可变捕获的惰性计划活得太久。
现实代码很少会直接告诉你“请使用 filter_map”。需求通常写成:“读取配置中的端口,空行忽略,格式错误要指出,最多接受五项。”如果一看到这句话就开始堆方法,很容易在错误处理或执行顺序上拐错弯。更稳的办法是先把需求拆成数据流问题。
先问输入是谁拥有。配置文本如果由调用方继续使用,就从借用开始,迭代项多半是 &str。再问每个输入可能产生几项:空行产生零项,正常行产生一项。然后问失败是数据还是控制流:格式错误不能忽略,应该让整次解析返回 Err。最后才是数量限制:最多接受五个有效结果,到达五个后是否还需要检查后面的坏数据?这个业务决定会影响 take 的位置。
下面的实现选择“只读取前五个非空配置项,后面的内容不检查”:
fn parse_first_five_ports(text: &str) -> Result<Vec<u16>, std::num::ParseIntError> {
text.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.take(5
bad 位于第五个有效端口之后,因此根本没被解析。如果需求是“整份配置都必须合法,但最多保留五项”,take(5) 就不能放在解析前。你需要先检查全部输入,再截取,或者用显式循环同时表达“验证全部”和“只保存五项”。这不是某个适配器更高级的问题,而是需求里有两种不同的“最多五项”。
filter_map 和收集 Result 常被放在一起比较,因为它们都能处理解析结果,但两者的承诺完全不同。
fn keep_valid_numbers(raw: &[&str]) -> Vec<i32> {
raw.iter()
.filter_map(|text| text.parse::<i32>().ok())
.collect()
}
fn require_all_numbers(
raw:
日志分析里忽略个别坏行也许可以接受,账单金额里忽略坏数据通常就是事故。不要因为 filter_map 少写几个字符,就让错误从类型里消失。先写清楚失败策略,再选方法。
还可以有第三种策略:保留成功项,同时收集所有错误。这时标准的短路 collect::<Result<...>>() 不符合需求,因为它在第一个错误处停止。你可以用 fold 累积两个向量,或者写显式循环。显式循环往往更直白:
fn main() {
let raw = ["10", "x", "20", "y"];
let mut numbers = Vec::new();
let mut errors = Vec::new();
for text in raw {
match text.parse::<i32>() {
这段循环不比一条复杂的 fold 链低级。它把两个输出和错误处理摆在明面上,维护者不用先解读一个二元累积器。
假设你要把订单向量转换成订单编号。如果订单之后不再使用,按值迭代可以直接移动字段;如果后面还要用订单,就只能借用字段或克隆需要拥有的部分。
#[derive(Debug)]
struct Order {
id: String,
amount: u32,
}
fn main() {
let orders = vec![
Order { id: "A-10".into(), amount: 120 },
Order { id: "B-20".into(), amount: 80
order.id 能被直接移出,是因为闭包拥有整个 Order。若改成 orders.iter(),闭包拿到 &Order,就不能从共享引用背后把 String 搬走。可以收集 &str,让结果借用原订单;也可以 .map(|order| order.id.clone()),用复制成本换取独立所有权。
#[derive(Debug)]
struct Order {
id: String,
amount: u32,
}
fn main() {
let orders = vec![
Order { id: "A-10".into(), amount: 120 },
Order { id: "B-20".into(), amount: 80
这里 ids 的有效期不能超过 orders。编译器会负责守住这个关系。你省下了克隆,也接受了结果依赖原数据存活的约束。哪个版本正确,要看结果准备活多久,而不是看谁更“Rust”。
如果只要第一条满足条件的记录,find 直接表达意图,还能短路。先 filter(...).collect::<Vec<_>>() 再取第一项,会检查并保存所有匹配项,做了多余工作。
fn main() {
let first = [3, 7, 12, 18]
.into_iter()
.find(|n| *n % 2 == 0);
assert_eq!(first, Some(12));
}如果只要知道是否存在,用 any,不要 find(...).is_some() 来暗示你需要值;如果要验证全部,用 all。这些写法的运行成本可能接近,但读者看到方法名就能知道你要布尔判断、第一项还是完整集合。
scan 和 fold 都能维护状态,区别在于输出形状。fold 把整串输入压成一个最终累积值;scan 在推进状态的同时,可以为每个输入产生一个中间输出。
计算最终总和,用 sum 或 fold。计算每一步余额,用 scan:
fn main() {
let balances: Vec<i32> = [100, -30, 20, -50]
.into_iter()
.scan(0, |balance, change| {
*balance += change;
Some(*balance)
})
如果状态机有多种分支、需要回退、要报告精确位置,显式结构体和 next 往往比一个巨大的 scan 闭包好维护。scan 适合局部、线性的状态,不是所有解析器都应该压进去。
链的顺序不是排版选择。filter().take(10) 表示找到前十个符合条件的项;take(10).filter() 表示只检查原始输入的前十项,再从中筛选。map().find() 会转换直到找到目标;如果条件能在转换前判断,先 find 可能少做昂贵转换。
fn main() {
let first_three_evens: Vec<_> = (1..)
.filter(|n| n % 2 == 0)
.take(3)
.collect();
let evens_in_first_three: Vec<_> = (1..)
遇到链顺序拿不准,可以用一句自然语言逐段朗读。“从正整数开始,筛出偶数,取前三个,收集。”如果读出来的意思和需求不同,方法顺序就该调整。
迭代器报错经常很长,因为最终类型里会嵌套 Map<Filter<...>>。你不用一次读懂整串名字。先找错误发生在哪类边界:引用和值、闭包返回值、目标集合,还是借用持续时间。
collect 无法推断目标类型如果错误说需要类型注解,或者存在多个 FromIterator 实现,先给 collect 结果加类型。下面两种任选一种:
fn main() {
let a: Vec<i32> = (1..4).collect();
let b = (1..4).collect::<Vec<i32>>();
assert_eq!(a, b);
}不要急着把链上每个闭包参数都标满类型。collect 的目标一旦确定,Item 约束往往会沿链向前传播,前面的数字类型也会一起确定。只在仍有歧义的地方补第二个标注。
某些错误看起来发生在 collect,其实是上游项形状不对。例如收集 HashMap 时,Item 必须是键值对。若你只产生字符串,给 collect::<HashMap<_, _>>() 再多提示也没用。先检查最后一段迭代器产生的是不是 (K, V)。
下面的意图是把字符串放进新向量,但 iter() 只给 &String:
fn main() {
let names = vec![String::from("Ada"), String::from("Grace")];
// let copied: Vec<String> = names.iter().map(|name| *name).collect();
}*name 试图把 String 从共享引用后面搬走。允许这样做的话,原向量还认为自己拥有字符串,内部元素却已经不完整。编译器拒绝是对的。
修复取决于意图。原向量不再需要,就使用 into_iter() 取得 String;原向量必须保留,新向量又要独立拥有字符串,就 cloned();结果只在原向量存活期间使用,可以收集 &String 或 &str。没有一个修复能同时做到“原值不动、零复制、新结果永久独立”,所有权规则只是把这个不可能三角摆到了台面上。
filter 为什么看起来多一层 &filter 必须先借用项做判断,保留时再把原项交下去,所以判断闭包接收 &Self::Item。当上游 Item = i32,参数是 &i32;上游 Item = &i32,参数就是 &&i32。
碰到比较错误时,可以暂时显式标注:
fn main() {
let values = [1, 2, 3, 4];
let result: Vec<&i32> = values
.iter()
.filter(|number: &&i32| **number >= 3)
.collect
正式代码可以利用自动解引用或模式匹配写得更短,但调试阶段把 &&i32 写出来很有帮助。你看到的不是编译器平白多塞一层,而是 iter 的借用和 filter 的观察借用叠在了一起。
map 的闭包可以返回任何新项,filter 的闭包必须返回 bool,filter_map 必须返回 Option<B>,scan 必须返回 Option<B>。大括号里的最后一个表达式决定闭包返回值;多写一个分号,类型可能就从你想要的值变成 ()。
fn main() {
let doubled: Vec<i32> = [1, 2, 3]
.into_iter()
.map(|n| {
let result = n * 2;
result
})
.collect();
assert_eq!(doubled, [
如果最后写成 result;,闭包返回 (),最终就会得到 Iterator<Item = ()>。错误也许在 collect::<Vec<i32>>() 才爆出来,但源头是闭包末尾的分号。看到 “expected i32, found ()” 时,先检查闭包最后一行。
迭代器可以借用输入,但被借用的数据必须活得足够久。下面这种函数无法成立:
// fn wrong() -> impl Iterator<Item = &'static str> {
// let text = String::from("rust iterator");
// text.split_whitespace()
// }函数返回后,局部 String 会被释放,返回的迭代器却还想产生指向它内部的 &str。编译器不会让悬空引用离开函数。
如果文本由调用方提供,可以让返回迭代器的生命周期跟输入绑定:
fn words(text: &str) -> impl Iterator<Item = &str> {
text.split_whitespace()
}
fn main() {
let text = String::from("rust iterator is lazy");
let result: Vec<_> = words(&text)
如果函数必须自己创建文本,又要把结果交出去,就收集成拥有数据的 Vec<String>,或者设计一个同时拥有文本和遍历状态的结构。单纯返回借用局部变量的迭代器没有安全实现。
惰性会让借用比你直觉中持续更久。创建一条基于 iter_mut() 的链,只是把借用存进计划;直到计划最后一次使用,这个可变借用都可能存在。
fn main() {
let mut values = vec![1, 2, 3];
let plan = values.iter_mut().map(|n| {
*n *= 10;
});
// println!("{values:?}"); // plan 后面还会用,不能同时读取 values
plan.for_each(drop);
println!
这里 map 闭包返回 (),for_each(drop) 只是推动每一项,让闭包执行。虽然代码能工作,表达原地修改时还是直接 for 更清楚。这个例子真正想说明的是:链没执行不等于借用没建立。
当错误跨过十几个适配器才在末尾出现,可以这样处理:
先把链截在出错位置前,给中间变量补一个你认为正确的 Item 去向,例如先收集成一个临时 Vec<_>,确认上半段能否编译。调试完成后再决定是否去掉临时分配。
把复杂闭包改成有参数类型和返回类型的命名函数。这样错误会落在函数体或函数签名附近,不会一直传到 collect。
对引用层数拿不准时,暂时写出 |x: &&T| 之类的显式类型,或者手动调用一次 next(),看返回的是 Option<T>、Option<&T> 还是别的形状。
长错误信息让人烦躁很正常。把链切开以后,你会发现编译器多数时候只是在某个具体边界上说:“这里交出来的是引用,你却要值”,或者“这里可能收集成很多类型,你还没告诉我选哪个”。先解决最靠前的类型分歧,后面的错误常会一起消失。
Rust 的迭代器适配器通常是具体泛型类型,编译器可以内联闭包、合并状态机,生成与手写循环非常接近的代码。惰性还避免了每个适配器都创建中间 Vec。不过,这不意味着“只要写成链就一定更快”。
先看分配边界。下面的第一种写法只有最后一次 collect:
fn one_allocation_boundary(values: &[i32]) -> Vec<i32> {
values
.iter()
.copied()
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.collect()
如果为了“分步骤”在中间反复 collect::<Vec<_>>(),就会增加分配、遍历和内存占用。需要调试中间值时可以暂时收集,正式代码则可以先考虑命名迭代器变量或使用 inspect。
但一条二十行的链也未必是好代码。闭包里同时解析、更新外部状态、记录日志、处理错误,读者必须在脑中模拟多个层次。此时拆成几个有名字的函数,或者使用显式 for,经常更可靠。迭代器的目标是表达数据流,不是参加“谁能少写大括号”的比赛。
再看复制与克隆。能借用就先借用,确实要拥有时再 cloned();先筛选再克隆,能减少无用工作。Copy 很便宜也不是完全免费,大结构即使实现 Copy,频繁复制仍可能影响性能。最终要用 release 构建和真实数据测量,不要只凭写法判断。
短路是另一个实在的性能来源。查找一个满足条件的元素,用 find 不必先收集全部结果;验证条件用 all,遇到第一个反例就停。相反,collect、fold、sum 通常会走完有限迭代器,对无限迭代器可能永远不返回。
副作用会让惰性和短路变得更难推理。
fn main() {
let visited: Vec<i32> = [1, 2, 3, 4, 5]
.into_iter()
.inspect(|n| println!("访问 {n}"))
.take(2)
.collect();
这里只打印两次。把 inspect 移到别的位置、把 take 换成 filter、把消费者删掉,副作用次数都会变化。必须执行一次且只能执行一次的业务动作,不适合依赖这种隐含推进。
最后是错误信息。长迭代器链报类型错误时,错误位置可能落在 collect,真正的问题却在前面某个闭包。可以暂时拆开链,为中间变量加类型:
fn main() {
let parsed = ["1", "2", "3"]
.into_iter()
.map(|text| text.parse::<i32>());
let values: Result<Vec<i32>, _> = parsed.collect();
assert_eq!
编译器不是要求你永远写出最复杂的类型,它只需要在关键分叉处得到足够信息。拆开一条链不是认输,而是在给人和编译器都加路标。
collect迭代器适配器本身通常不保存全部元素,Map、Filter 这类值只需要保存上游迭代器和闭包状态。真正明显的内存边界经常出现在 collect::<Vec<_>>()、把字符串重新拼接、克隆拥有型元素这些地方。但也不能简单地数 collect 次数就下结论。
例如,有时你故意在中间收集,是因为下一步需要排序、随机访问或重复遍历。排序必须看到全部数据,继续保持纯流式并不会让问题消失。好的优化不是“删除所有中间向量”,而是确认每次物化有没有功能理由,生命周期能否缩短,元素是否可以移动而非克隆。
迭代器还可以通过 size_hint() 报告剩余项数量的下界和可选上界。集合在收集时可以利用这些信息预留容量,减少扩容次数。map 通常保持数量关系,filter 则无法预先知道有多少项会通过。这个提示不是正确性保证,标准集合的具体分配策略也不该成为业务逻辑依赖。
自己实现迭代器时,错误的 size_hint 可能让调用方做出糟糕的预分配,某些更强 trait 的错误实现甚至会破坏依赖它们的假设。没有把上下界算清楚,就保留默认实现。先写对 next,再用测试覆盖空输入、单元素、边界值和结束状态,最后才考虑提示优化。
如果筛选条件很便宜,转换很昂贵,而且筛选不依赖转换结果,先 filter 再 map 通常能少做工作。比如先检查文件扩展名,再读取并解析文件,比先解析所有文件再筛选合理。但如果筛选条件本身必须基于解析后的字段,顺序就不能为了性能硬换。
同样,take 放得越靠前,通常处理的项越少,却可能改变含义。先取十条再筛选,得到的是“前十条中的合格项”;先筛选再取十条,得到的是“最先出现的十条合格项”。性能优化不能越过这条语义边界。
短路闭包内部的成本也值得看。any(|item| cheap(item) && expensive(item)) 会利用布尔运算自身的短路,只有便宜条件通过才做昂贵检查。把两个条件调换,结果可能相同,成本却不同。不过,条件函数若带副作用,换序还会改变行为;这又提醒我们,判断闭包越纯粹,优化空间越容易推理。
手写循环有时能更自然地缓存中间结果、复用缓冲区或把多个输出一起更新。迭代器链也能做到,但如果需要大量外部可变状态,强行链式化可能让借用关系和分支变得复杂。先写出清楚版本,再用分析工具定位热点。一个在非热点路径上快了几个百分点、却让错误处理难懂的改写,通常不值。
无限迭代器并不危险,失去终止条件才危险。范围 (0..)、repeat(value)、不断生成新状态的序列都可以安全使用,只要下游有可证明会结束的 take,或者短路条件会在有限步内命中。
“我觉得总能找到”不是很可靠的证明。(1..).find(|n| n == target) 在 target 为正数时能结束,若 target 来自未经验证的负数输入,就会永远向前搜索。更稳的写法是先验证条件,或把搜索空间限制为业务允许的范围。对于外部数据驱动的服务,时间上限和数量上限通常都应该显式存在。
即使输入有限,算法也可能看起来像卡住。flat_map 的某个内层迭代器若无限,后面的外层项永远没有机会出现;cycle() 会把有限序列变成无限重复;在这些链上调用 collect、count、sum 或完整 fold 都不会自然结束。
审查链时可以从消费者向前问:它需要上游返回多少次 Some 才能决定结果?take(100).collect() 最多一百次;any 取决于第一次命中;普通 collect 要等 None。再问上游是否保证最终返回 None。这两问能提前发现不少只在生产数据上出现的“无限等待”。
拆分长链不等于一定要创建中间集合。迭代器本身也能绑定到有意义的变量:
fn main() {
let raw = [" 10", "bad", "20 ", "-5"];
let trimmed = raw.into_iter().map(str::trim);
let parsed = trimmed.filter_map(|text| text.parse::<i32>()
这几行仍然只有最后一个集合分配,却给每个阶段起了名字。调试时可以在某一段加入 inspect,类型错误也更容易定位。变量名要描述阶段含义,而不是叫 iter1、iter2,否则只是把一行拆成了四行。
命名函数也很有用。复杂闭包如果包含多个分支,可以提取成 fn parse_record(...) -> Result<...> 或 fn is_visible(...) -> bool。这样迭代器链保留流程轮廓,细节在函数内单独测试。相反,一个只出现一次、只有 |n| n * 2 的闭包没必要为了形式感提取。
代码评审时,我更愿意问“能不能在几秒内说出每一步的 Item 和失败方式”,而不是问链有几行。六个简单适配器可能非常清楚,两个塞满分支和副作用的闭包也可能很难维护。可读性的单位不是方法数量,而是读者需要同时记住多少状态。
在 map、filter、inspect 或 fold 闭包里写日志通常不会破坏正确性,但写数据库、发送消息、修改外部共享状态就需要更谨慎。惰性决定副作用何时开始,短路决定它做多少次,错误决定它停在哪里。迭代器不会自动提供事务。
假设你用 try_for_each 逐个写入五条记录,第三条失败。前两条很可能已经写入,后三条没有执行。返回 Err 只能描述遍历停止,不能把前两次写入撤销。若业务要求全有或全无,应先把输入验证并准备好,再交给事务一次提交,或者为已完成动作设计补偿逻辑。
日志也要理解位置差异。放在 filter 前表示“看到了这项输入”,放在 filter 后表示“这项通过了筛选”,放在写入成功之后才表示“这项已经处理完成”。三条日志看起来都像 inspect,语义却不同。日志文字应该和所在阶段一致,否则排障时会把“尝试过”误读成“成功了”。
多次消费也会重复副作用。普通迭代器通常只能向前走一次,但如果你重新创建同一条链,闭包会重新执行。把迭代器链当成纯数据查询时这很自然;一旦链里夹了外部动作,“重新跑一下看看”就可能造成重复写入。需要幂等性的场景,应让外部操作自身能识别重复请求,而不是寄希望于调用者永远只消费一次。
一条值得保留的迭代器链,通常能让你从左到右读出数据经历了什么,并且能快速指出所有权、错误和副作用在哪里发生。短不短只是表面,能不能准确推理才是判断标准。
下面几组练习不要求你炫技。先判断 Item 和所有权,再决定适配器与消费者。
你只想找出长度至少为五的名字,同时后面还要继续使用原向量。下面的代码为什么失败,怎样修复才不需要克隆所有字符串?
fn main() {
let names = vec![
String::from("Ada"),
String::from("Grace"),
String::from("Linus"),
];
let long: Vec<String> = names
.into_iter()
.filter(
把 ['8', '13', 'x', '21'] 这一类文本输入解析成整数向量。遇到坏数据时,不允许静默跳过,应该返回解析错误。
下面代码执行后,rest 是什么?
fn main() {
let mut numbers = [2, 4, 6, 7, 8, 10].into_iter();
let all_even_so_far = numbers.all(|n| n % 2 == 0);
let rest: Vec<_> =
学到这里,你不需要把每个适配器的签名背下来。真正要形成的是一套检查顺序:先看数据是借用还是移动,再确认 Item,然后判断链里哪些步骤只是描述、哪个步骤会推进,最后检查是否短路、是否有副作用、结果类型是否给了编译器足够信息。
实际写代码时,可以先从最朴素的 for 循环开始。等输入、输出和错误策略都稳定后,再把连续的“转换、筛选、截取、汇总”整理成迭代器链。这样做不是退回初级写法,反而能避免在需求还没想清楚时,被一串方法名带着走。反过来,如果现有链已经清楚,就没有必要为了调试习惯全部改回下标循环。Rust 同时提供两套表达方式,是让你按问题选择,不是在逼你站队。
团队代码还应约定错误和副作用的可见度。看到 filter_map(Result::ok),评审者应该追问丢弃错误是否符合业务;看到 inspect 写外部状态,应该确认短路与重试会不会改变次数;看到 into_iter(),应该确认调用方是否真的愿意交出所有权。这些问题比“链式写法够不够地道”更接近线上行为。
当一条链让你开始怀疑人生时,就把它拆开,给中间值标上类型,甚至手动调用两次 next()。编译器这个搭档有时说话很长,但它追问的通常很朴素:这一项是什么,归谁,什么时候真的被取走。你能回答这三个问题,迭代器就从一串魔法调用,变回了一个可以一步步推理的状态机。
最后再恢复惰性链,并检查中间调试用的 collect 是否引入了不必要分配。修复类型问题和优化性能是两个步骤,不必一次完成。
原始数量:3
长名字:["Grace", "Linus"]如果题目明确允许忽略坏数据,才改用 filter_map(|text| text.parse().ok())。这两个版本的业务语义不同。