迭代器与闭包

一句话理解

Rust 的迭代器是惰性的、零成本抽象的:map/filter 只是包装一层适配器,直到遇到消费者(collect/sum/for)才真正开始跑。编译优化后,它与手写 for 循环通常生成同样的机器码。

闭包按捕获方式分三类,对应 Fn / FnMut / FnOnce 三个 trait。理解这三层,就理解了闭包为什么有时候会被”移动走”。

1. 迭代器的三段式

创建迭代器  →  适配器(惰性,可链式)  →  消费者(真正执行)
iter()          map / filter / take       collect / sum / for
let 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. 小结

关键判断

  1. 迭代器惰性:不消费就不执行
  2. iter / iter_mut / into_iter 对应借用 / 可变借用 / 拿走所有权
  3. collect 能收集到 Result/Option,天然短路——处理批量解析时非常有用
  4. 闭包三层:Fn ⊃ FnMut ⊃ FnOnce;move 只改变捕获方式
  5. 迭代器是零成本抽象,优先用它而不是手写索引循环

相关笔记