Rust Iterator 实用技巧:少写循环,把数据流写清楚
wxk1991 Lv5

Rust Iterator 实用技巧:少写循环,把数据流写清楚

Rust 里的 Iterator 不是“语法糖玩具”。

它是日常写 Rust 最值得熟练掌握的工具之一。

很多刚开始写 Rust 的代码会长这样:

1
2
3
4
5
6
7
let mut result = Vec::new();

for item in items {
if item.active {
result.push(item.name);
}
}

这当然能跑。

但 Rust 更推荐你把“怎么遍历”交给 Iterator,把注意力放在“数据怎么流动”上:

1
2
3
4
5
let result: Vec<_> = items
.into_iter()
.filter(|item| item.active)
.map(|item| item.name)
.collect();

这篇文章不讲 Iterator 的完整文档,只整理几个真实项目里最常用、最容易踩坑的技巧。


一、先分清 iter、into_iter、iter_mut

Rust Iterator 最容易混乱的地方,是这三个方法:

1
2
3
iter
into_iter
iter_mut

它们的区别非常关键。

1. iter:只读借用

1
2
3
4
5
6
7
let names = vec!["Alice".to_string(), "Bob".to_string()];

for name in names.iter() {
println!("{}", name);
}

println!("len = {}", names.len());

iter() 产生的是引用:

1
&String

所以循环结束后,names 还可以继续使用。

适合场景:

  • 只读遍历
  • 不想移动集合里的值
  • 后面还要继续使用原集合

2. into_iter:消费所有权

1
2
3
4
5
6
let names = vec!["Alice".to_string(), "Bob".to_string()];

let upper_names: Vec<_> = names
.into_iter()
.map(|name| name.to_uppercase())
.collect();

into_iter() 会消费集合。

这里 name 的类型是:

1
String

它拿到了元素所有权,所以可以直接移动、转换、放进新集合。

适合场景:

  • 原集合后面不用了
  • 想避免不必要的 clone
  • 要把元素转换成另一个集合

3. iter_mut:可变借用

1
2
3
4
5
let mut scores = vec![80, 90, 70];

for score in scores.iter_mut() {
*score += 5;
}

iter_mut() 产生的是:

1
&mut i32

适合原地修改集合内容。

这三个方法不要混着用。写 Iterator 前先问自己:

1
我要读取、消费,还是修改?

答案清楚了,方法基本也就清楚了。


二、用 filter_map 少写一层 match

很多代码会先过滤,再解包:

1
2
3
4
5
6
7
8
9
let values = vec!["1", "a", "2", "b", "3"];

let mut nums = Vec::new();

for value in values {
if let Ok(num) = value.parse::<i32>() {
nums.push(num);
}
}

Iterator 可以写得更干净:

1
2
3
4
let nums: Vec<i32> = values
.into_iter()
.filter_map(|value| value.parse::<i32>().ok())
.collect();

filter_map 的意思是:

1
2
返回 Some,就保留并解包
返回 None,就丢弃

它特别适合处理:

  • 字符串解析
  • 可选字段提取
  • 查找并转换
  • 跳过不合法数据

比如从用户列表里取邮箱:

1
2
3
4
let emails: Vec<&str> = users
.iter()
.filter_map(|user| user.email.as_deref())
.collect();

这里 as_deref() 可以把:

1
Option<String>

转换成:

1
Option<&str>

避免为了收集邮箱而 clone 字符串。


三、不要太早 collect

很多人写 Iterator 时,会习惯中间 collect 一次:

1
2
3
4
5
6
7
8
9
let active_users: Vec<_> = users
.iter()
.filter(|user| user.active)
.collect();

let names: Vec<_> = active_users
.iter()
.map(|user| user.name.clone())
.collect();

这通常没有必要。

可以直接连起来:

1
2
3
4
5
let names: Vec<_> = users
.iter()
.filter(|user| user.active)
.map(|user| user.name.as_str())
.collect();

Iterator 是惰性的。

如果不调用 collectcountsumfor_each 这类消费方法,中间的 mapfilter 并不会真正执行。

所以中间过早 collect 往往会带来:

  • 多一次内存分配
  • 多一个临时集合
  • 多一点所有权处理复杂度
  • 更难读的数据流

除非你确实需要复用中间结果,否则尽量让数据流一次走到底。


四、collect 需要类型提示

collect 很强,但它经常需要你告诉编译器目标类型。

比如:

1
2
3
4
let nums = vec!["1", "2", "3"]
.into_iter()
.map(|s| s.parse::<i32>().unwrap())
.collect();

这段代码可能编译不过,因为 Rust 不知道你想收集成:

1
2
3
Vec<i32>
HashSet<i32>
VecDeque<i32>

更明确的写法是:

1
2
3
4
let nums: Vec<i32> = vec!["1", "2", "3"]
.into_iter()
.map(|s| s.parse::<i32>().unwrap())
.collect();

或者:

1
2
3
4
let nums = vec!["1", "2", "3"]
.into_iter()
.map(|s| s.parse::<i32>().unwrap())
.collect::<Vec<_>>();

我更喜欢第一种。

因为变量类型写在左边,后面链式调用会更清楚。


五、any、all、find 比手写标志位清楚

不要这样写:

1
2
3
4
5
6
7
8
let mut has_admin = false;

for user in &users {
if user.role == "admin" {
has_admin = true;
break;
}
}

可以写成:

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
2
3
for (index, user) in users.iter().enumerate() {
println!("{}: {}", index, user.name);
}

需要两个集合一一对应时,用 zip

1
2
3
4
5
6
7
8
let names = vec!["Alice", "Bob"];
let scores = vec![90, 80];

let reports: Vec<_> = names
.into_iter()
.zip(scores)
.map(|(name, score)| format!("{}: {}", name, score))
.collect();

zip 会在较短的迭代器结束时停止。

如果两个集合长度必须一致,不要只依赖 zip,应该提前检查长度:

1
2
3
if names.len() != scores.len() {
return Err("names and scores length mismatch");
}

七、try_for_each 和 try_fold 适合处理可失败流程

如果循环里每一步都可能失败,很多人会这样写:

1
2
3
4
for path in paths {
let content = std::fs::read_to_string(path)?;
println!("{}", content.len());
}

这已经很好。

但如果你想保持 Iterator 风格,可以用 try_for_each

1
2
3
4
5
paths.iter().try_for_each(|path| -> std::io::Result<()> {
let content = std::fs::read_to_string(path)?;
println!("{}", content.len());
Ok(())
})?;

如果还要累加结果,可以用 try_fold

1
2
3
4
let total_size = paths.iter().try_fold(0usize, |acc, path| {
let content = std::fs::read_to_string(path)?;
Ok::<usize, std::io::Error>(acc + content.len())
})?;

try_fold 的价值是:

1
中途失败就返回错误,成功就继续累积。

不过要说实话:不是所有循环都应该硬改成 Iterator。

如果 for 循环更直观,就用 for

Rust 的目标不是炫技,而是写出清楚又可靠的代码。


八、inspect 适合临时观察数据流

调试 Iterator 链时,可以用 inspect

1
2
3
4
5
6
7
let nums: Vec<_> = vec![1, 2, 3, 4]
.into_iter()
.inspect(|n| println!("before filter: {}", n))
.filter(|n| n % 2 == 0)
.inspect(|n| println!("after filter: {}", n))
.map(|n| n * 10)
.collect();

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 代码就会比一堆手写循环更容易维护。


参考资料