Rust Iterator 实用技巧:少写循环,把数据流写清楚
Rust 里的 Iterator 不是“语法糖玩具”。
它是日常写 Rust 最值得熟练掌握的工具之一。
很多刚开始写 Rust 的代码会长这样:
1 | let mut result = Vec::new(); |
这当然能跑。
但 Rust 更推荐你把“怎么遍历”交给 Iterator,把注意力放在“数据怎么流动”上:
1 | let result: Vec<_> = items |
这篇文章不讲 Iterator 的完整文档,只整理几个真实项目里最常用、最容易踩坑的技巧。
一、先分清 iter、into_iter、iter_mut
Rust Iterator 最容易混乱的地方,是这三个方法:
1 | iter |
它们的区别非常关键。
1. iter:只读借用
1 | let names = vec!["Alice".to_string(), "Bob".to_string()]; |
iter() 产生的是引用:
1 | &String |
所以循环结束后,names 还可以继续使用。
适合场景:
- 只读遍历
- 不想移动集合里的值
- 后面还要继续使用原集合
2. into_iter:消费所有权
1 | let names = vec!["Alice".to_string(), "Bob".to_string()]; |
into_iter() 会消费集合。
这里 name 的类型是:
1 | String |
它拿到了元素所有权,所以可以直接移动、转换、放进新集合。
适合场景:
- 原集合后面不用了
- 想避免不必要的 clone
- 要把元素转换成另一个集合
3. iter_mut:可变借用
1 | let mut scores = vec![80, 90, 70]; |
iter_mut() 产生的是:
1 | &mut i32 |
适合原地修改集合内容。
这三个方法不要混着用。写 Iterator 前先问自己:
1 | 我要读取、消费,还是修改? |
答案清楚了,方法基本也就清楚了。
二、用 filter_map 少写一层 match
很多代码会先过滤,再解包:
1 | let values = vec!["1", "a", "2", "b", "3"]; |
Iterator 可以写得更干净:
1 | let nums: Vec<i32> = values |
filter_map 的意思是:
1 | 返回 Some,就保留并解包 |
它特别适合处理:
- 字符串解析
- 可选字段提取
- 查找并转换
- 跳过不合法数据
比如从用户列表里取邮箱:
1 | let emails: Vec<&str> = users |
这里 as_deref() 可以把:
1 | Option<String> |
转换成:
1 | Option<&str> |
避免为了收集邮箱而 clone 字符串。
三、不要太早 collect
很多人写 Iterator 时,会习惯中间 collect 一次:
1 | let active_users: Vec<_> = users |
这通常没有必要。
可以直接连起来:
1 | let names: Vec<_> = users |
Iterator 是惰性的。
如果不调用 collect、count、sum、for_each 这类消费方法,中间的 map、filter 并不会真正执行。
所以中间过早 collect 往往会带来:
- 多一次内存分配
- 多一个临时集合
- 多一点所有权处理复杂度
- 更难读的数据流
除非你确实需要复用中间结果,否则尽量让数据流一次走到底。
四、collect 需要类型提示
collect 很强,但它经常需要你告诉编译器目标类型。
比如:
1 | let nums = vec!["1", "2", "3"] |
这段代码可能编译不过,因为 Rust 不知道你想收集成:
1 | Vec<i32> |
更明确的写法是:
1 | let nums: Vec<i32> = vec!["1", "2", "3"] |
或者:
1 | let nums = vec!["1", "2", "3"] |
我更喜欢第一种。
因为变量类型写在左边,后面链式调用会更清楚。
五、any、all、find 比手写标志位清楚
不要这样写:
1 | let mut has_admin = false; |
可以写成:
1 | let has_admin = users.iter().any(|user| user.role == "admin"); |
检查全部满足:
1 | let all_active = users.iter().all(|user| user.active); |
查找某个元素:
1 | let admin = users.iter().find(|user| user.role == "admin"); |
这些方法的好处是:意图很明确。
any 一看就是“是否存在”。
all 一看就是“是否全部满足”。
find 一看就是“找第一个”。
代码越接近业务语言,维护成本越低。
六、enumerate 和 zip 很适合处理对应关系
需要下标时,不要手动维护计数器:
1 | for (index, user) in users.iter().enumerate() { |
需要两个集合一一对应时,用 zip:
1 | let names = vec!["Alice", "Bob"]; |
zip 会在较短的迭代器结束时停止。
如果两个集合长度必须一致,不要只依赖 zip,应该提前检查长度:
1 | if names.len() != scores.len() { |
七、try_for_each 和 try_fold 适合处理可失败流程
如果循环里每一步都可能失败,很多人会这样写:
1 | for path in paths { |
这已经很好。
但如果你想保持 Iterator 风格,可以用 try_for_each:
1 | paths.iter().try_for_each(|path| -> std::io::Result<()> { |
如果还要累加结果,可以用 try_fold:
1 | let total_size = paths.iter().try_fold(0usize, |acc, path| { |
try_fold 的价值是:
1 | 中途失败就返回错误,成功就继续累积。 |
不过要说实话:不是所有循环都应该硬改成 Iterator。
如果 for 循环更直观,就用 for。
Rust 的目标不是炫技,而是写出清楚又可靠的代码。
八、inspect 适合临时观察数据流
调试 Iterator 链时,可以用 inspect:
1 | let nums: Vec<_> = vec![1, 2, 3, 4] |
inspect 不改变数据,只是让你在链路中间看一眼。
它适合临时调试。
不要在正式业务逻辑里大量依赖 inspect 做副作用,否则 Iterator 链会变得难懂。
九、Iterator 不是性能敌人
很多人担心 Iterator 会不会比 for 慢。
Rust 官方 Book 里专门提到,闭包和 Iterator 是零成本抽象的一部分。很多情况下,Iterator 链会被编译器优化成和手写循环接近的机器码。
真正影响性能的,通常不是 map 还是 for,而是:
- 是否不必要地 clone
- 是否过早 collect
- 是否重复分配
- 是否用了错误的数据结构
- 是否在热路径里做了大量字符串格式化
也就是说,不要为了“看起来底层”就拒绝 Iterator。
更好的标准是:
1 | 先写清楚,再用真实场景 benchmark。 |
十、我的建议
日常写 Rust,可以先记住这几条:
- 只读用
iter - 消费用
into_iter - 原地修改用
iter_mut - 跳过无效值用
filter_map - 不要中间过早
collect - 查存在用
any - 查第一个用
find - 需要下标用
enumerate - 可失败累加用
try_fold - 调试链路用
inspect
Iterator 的价值不是让代码更短。
而是让数据流更清楚。
当你能一眼看出“先过滤、再转换、最后收集”,Rust 代码就会比一堆手写循环更容易维护。