迭代器与闭包
一句话理解
Rust 的迭代器是惰性的、零成本抽象的:
map/filter只是包装一层适配器,直到遇到消费者(collect/sum/for)才真正开始跑。编译优化后,它与手写for循环通常生成同样的机器码。闭包按捕获方式分三类,对应
Fn/FnMut/FnOnce三个 trait。理解这三层,就理解了闭包为什么有时候会被”移动走”。
1. 迭代器的三段式
创建迭代器 → 适配器(惰性,可链式) → 消费者(真正执行)
iter() map / filter / take collect / sum / forlet v = vec![1, 2, 3, 4, 5];
// 什么都不会发生:只是建了个适配器链
let it = v.iter().map(|x| x * 2).filter(|x| x > &4);
// 到这里才执行
let result: Vec<i32> = it.collect();忘记消费 = 什么都不做
v.iter().map(|x| println!("{x}"));什么都不会打印。编译器会给你unused_must_use警告——别忽略它。
2. IntoIterator 与三种迭代形式
for x in collection 会脱糖成 collection.into_iter()。因此三种形式对应三种所有权:
| 写法 | Item | 所有权 |
|---|---|---|
v.iter() | &T | 只读借用,原集合仍可用 |
v.iter_mut() | &mut T | 可变借用,可原地修改 |
v.into_iter() | T | 消耗集合,常用于把 Vec<T> 转成别的集合 |
&v | &T | 等价于 v.iter() |
&mut v | &mut T | 等价于 v.iter_mut() |
let mut v = vec![1, 2, 3];
for x in &v { println!("{x}"); } // 只读
for x in &mut v { *x *= 2; } // 原地改
for x in v { println!("{x}"); } // v 被消耗,之后不能再用
into_iter()是避免 clone 的关键想把
Vec<String>变成Vec<usize>,用into_iter()逐项移动所有权;用iter()就只能拿到&String。判断标准:这个集合后面还要不要用?
3. 常用适配器
| 适配器 | 作用 |
|---|---|
map / flat_map | 变换 / 展开一层(返回迭代器的映射) |
filter / filter_map | 过滤 / 过滤 + 变换 + 丢掉 None |
take / take_while / skip / skip_while | 截断与跳过 |
zip / chain / enumerate | 配对 / 连接 / 带下标 |
rev / step_by / cycle | 反向 / 步长 / 循环 |
peekable | 允许 peek() 预看下一个 |
scan | 带状态的映射(累加器等) |
chain | 拼接两个迭代器 |
inspect | 调试用:不改值地偷看(类似 tap) |
切片上还有几个非 trait 方法:windows(n)、chunks(n)、chunks_exact(n)、split、split_at。
let text = "a=1,b=2,c=3";
let pairs: Vec<(&str, i32)> = text
.split(',')
.filter_map(|kv| {
let (k, v) = kv.split_once('=')?;
Some((k, v.parse().ok()?))
})
.collect();filter_map 在这里一次完成了”拆分 + 解析 + 丢弃失败项”,比 filter().map() 更直接。
4. 常用消费者
| 消费者 | 作用 |
|---|---|
collect | 收集成集合 |
sum / product | 求和 / 求积(需要 Sum/Product) |
count / min / max / min_by_key / max_by_key | 统计与极值 |
any / all / find / position | 判断与查找(短路) |
fold / reduce | 累积(fold 带初值,reduce 用首元素) |
for_each | 副作用 |
try_fold / try_for_each | 可失败的累积 |
collect 能收集到 Result,这是它最有用的隐藏能力:
let nums: Result<Vec<i32>, _> = ["1", "2", "x"]
.iter()
.map(|s| s.parse::<i32>())
.collect();
assert!(nums.is_err()); // 遇到第一个 Err 就短路同理还有 collect::<Option<Vec<_>>>()、collect::<HashMap<_, _>>()、collect::<String>()。
5. 闭包与三个捕获 trait
let name = String::from("Rust");
let greet = || println!("hello {name}"); // 不可变借用 name
greet();
println!("{name}"); // ✅ 仍然可用编译器会为每个闭包生成一个匿名结构体,并按”最小够用”的原则选择捕获方式:
| Trait | 捕获方式 | 能调用几次 | 典型场景 |
|---|---|---|---|
Fn | 不可变借用 &T | 任意次 | 只读的映射、过滤 |
FnMut | 可变借用 &mut T | 任意次(需 mut 绑定) | 累加、sort_by |
FnOnce | 拿走所有权 T | 一次 | 消耗捕获变量、thread::spawn |
三者是包含关系:Fn: FnMut: FnOnce。参数要”尽量宽”,接收方要”尽量严”:
fn apply_once<F: FnOnce() -> String>(f: F) -> String { f() } // 最宽
fn apply_mut<F: FnMut(i32) -> i32>(mut f: F) -> i32 { f(1); f(2) } // 中
fn apply<F: Fn(i32) -> i32>(f: F) -> i32 { f(1) } // 最严
move关键字
move || ...强制按值捕获。它不代表”闭包会跑很多次”,只代表捕获方式。
thread::spawn(move || ...)必须用move,因为线程可能比当前作用域活得久(见 Send 与 Sync)。
返回闭包时,如果类型无法写出,用 impl Trait 或装箱:
// 单一具体类型
fn adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
// 需要返回不同类型(分支)时,装箱
fn pick(flag: bool) -> Box<dyn Fn(i32) -> i32> {
if flag { Box::new(|x| x + 1) } else { Box::new(|x| x * 2) }
}返回
impl Fn要求单一类型和
impl Trait的一般规则一样:不能在不同分支返回不同闭包类型,否则需要Box<dyn Fn>。
6. 迭代器借用与常见坑
① 迭代中修改集合
let mut v = vec![1, 2, 3];
// for x in &v { v.push(*x); } // ❌ E0502:不可变借用期间又可变借用修法:先收集到新 Vec,或按索引访问(注意索引失效),或用 retain / drain / mem::take。
② iter_mut 里再借整个集合
let mut v = vec![1, 2, 3];
for x in v.iter_mut() {
// v.push(0); // ❌ 已经可变借出,不能再借
*x += 1; // ✅ 只操作当前元素
}③ 手写索引循环不如迭代器
// 不推荐:每次访问都要做边界检查,且容易写错
for i in 0..v.len() { process(v[i]); }
// 推荐:边界检查可以被消除
for x in &v { process(*x); }Claude 提示:clippy 有 needless_range_loop 规则专门提醒这个。
④ for_each 不要用来写带 ? 的逻辑
for_each 的闭包返回值被丢弃,? 用不了。要能提前退出,用 try_for_each 或普通 for 循环。
7. 自己实现一个迭代器
只需实现 next,其余适配器全都自动获得:
struct Counter { count: u32, max: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
// 可选:给 collect 等提供容量提示,能省一次重新分配
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.max - self.count) as usize;
(remaining, Some(remaining))
}
}size_hint 值得实现:collect 会用它预分配容量。对 ExactSizeIterator 还可以额外实现 len()。
8. 性能:迭代器真的不比循环慢
// 三种写法在 release 下通常生成相同机器码
let sum1: i64 = v.iter().map(|&x| x as i64).sum();
let mut sum2 = 0i64;
for &x in &v { sum2 += x as i64; }
let mut sum3 = 0i64;
for i in 0..v.len() { sum3 += v[i] as i64; } // 这种反而可能更慢- 适配器会被内联、单态化,边界检查常被消除
- 真正的性能差别来自算法与分配,不是”用了迭代器”
- 但要注意:过度链式会拖慢编译,且调试时调用栈难读
调试友好的技巧
在长链中间插
.inspect(|x| println!("{x:?}")),比打断点方便,也容易删掉。
9. 小结
关键判断
- 迭代器惰性:不消费就不执行
iter/iter_mut/into_iter对应借用 / 可变借用 / 拿走所有权collect能收集到Result/Option,天然短路——处理批量解析时非常有用- 闭包三层:
Fn⊃FnMut⊃FnOnce;move只改变捕获方式- 迭代器是零成本抽象,优先用它而不是手写索引循环