方法链式调用
本教程共 78 篇 · 第 51 篇 · 更新于 2026-08-08 · 约 11 分钟阅读
本节目标:把 map、filter、zip、enumerate、fold 等适配器串成一条链式流水线,理解”惰性直到消费者收尾”的机制,并知道迭代器是零成本的。
前面两章分别讲了迭代器基础和用迭代器改写循环。这一章把多个适配器连成一条链,看看 Rust 函数式写法的完整形态。
1-1 什么是链式调用
迭代器适配器返回的是新迭代器,所以可以一个接一个写下去,形成流水线:
let v = vec![1, 2, 3, 4, 5, 6];
let result: Vec<_> = v.iter()
.map(|x| x * 2) // 翻倍:[2,4,6,8,10,12]
.filter(|x| *x > 5) // 只留大于5:[6,8,10,12]
.collect(); // 收成 Vec
assert_eq!(result, vec![6, 8, 10, 12]);
从 map 到 filter 再到 collect,每个环节处理上一步的输出,数据像在流水线上流动。这就是方法链式调用。
1-2 惰性:链在消费者处才启动
关键要记住:链里的适配器(map、filter 等)全是惰性的,它们只规划、不执行。整条链一直”睡着”,直到遇到消费者适配器(sum、collect、product 等)才真正跑起来。
let v = vec![1, 2, 3];
v.iter().map(|x| x + 1).filter(|x| *x > 1); // 警告:什么都没发生
上面这行编译器会警告”未使用的迭代器”。因为没有消费者收尾,规划作废。加上 .collect() 或 .sum(),流水线才通电。
Note这对性能是好事:你在建链的过程中不花任何遍历成本,只有最后
collect一次把活全干了,而且编译器还能把整条链优化掉。
1-3 zip:把两个迭代器配对
zip 把两个迭代器并排”拉链”在一起,产出 (A, B) 元组:
use std::collections::HashMap;
fn main() {
let names = ["Sun", "Fei"];
let ages = [18, 20];
let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();
println!("{:?}", folks); // {"Sun": 18, "Fei": 20}
}
zip 之后是形如 [(name, age), ...] 的迭代器,再用 collect 收成 HashMap——这是把两个并列数组合成映射表的惯用法。
1-4 enumerate:带上索引
遍历时想同时拿到”索引 + 值”,用 enumerate:
let v = vec![10, 20, 30];
for (i, val) in v.iter().enumerate() {
println!("第{}个是{}", i, val);
}
enumerate 把每个元素变成 (索引, 值) 元组。它也是适配器,所以能继续接别的:
let v = vec![10, 20, 30, 40];
let sum: i64 = v.iter()
.enumerate()
.filter(|&(idx, _)| idx % 2 == 0) // 只留偶数索引:[10, 30]
.map(|(_, val)| *val as i64)
.sum();
println!("{sum}"); // 40
这里 idx % 2 == 0 配合 filter 每两个留一个,再把值取出来求和。链式里每个环节各管一摊。
1-5 fold:自定义累积
sum 只能加。fold 是任意形式的累积,你给初值和累积闭包:
let v = vec![1, 2, 3, 4];
let result = v.iter().fold(0, |acc, x| acc + x * x);
println!("{result}"); // 0 + 1 + 4 + 9 + 16 = 30(平方和)
fold(初值, |累加器, 元素| 新累加器):从初值开始,对每个元素更新累加器。sum 其实就是 fold 的特例。
1-6 串成一条完整流水线
把前面几个凑一起,演示一条链解决”配对后筛选再累积”:
let a = [1, 2, 3, 4, 5];
let b = [10, 20, 30, 40, 50];
let total: i32 = a.iter()
.zip(b.iter()) // [(1,10),(2,20),...]
.map(|(x, y)| x + y) // 两两相加:[11,22,33,44,55]
.filter(|s| s % 2 == 0) // 只留偶数:[22,44]
.sum(); // 66
println!("{total}"); // 66
zip 配对、map 相加、filter 筛偶、sum 收尾——五个环节一条链,意图和顺序都一目了然,换成 for 循环得嵌套好几层。
1-7 自己实现 Iterator
理解了适配器,不妨看看迭代器怎么造。只要为类型实现 Iterator、写好 next 即可:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter { Counter { count: 0 } }
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut c = Counter::new();
assert_eq!(c.next(), Some(1));
assert_eq!(c.next(), Some(2));
assert_eq!(c.next(), None); // 超过5后停止
}
next 每次加一并返回,超过 5 返回 None。只要实现 next,map/filter/sum 这些都有默认实现,自动可用:
let s: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
println!("{s}"); // 18
注意这里 zip、map、filter 全是适配器,sum 是消费者——又一次印证”链到消费者才执行”。
1-8 性能:迭代器是零成本的
有人担心链式调用会不会比 for 慢。答案是不会。迭代器是 Rust 的零成本抽象:抽象本身不引入运行时开销。
官方基准测试对比过”for 循环求和”与”迭代器求和”,迭代器反而略快一点。编译器会对迭代器做循环展开、向量化、消除边界检查等优化,最终生成的机器码和手写循环一样高效。所以放心用——表达力更强,速度却不打折。
Tip想用好迭代器,关键是熟悉它的方法表。遇到”要对集合做什么”,先去标准库
Iterator文档翻有没有现成适配器,往往一行链式就搞定,比自己写循环强。
1-9 链里遇到错误怎么办
当链中的每一步都可能失败(返回 Result)时,直接用 map 会得到 Iterator<Item=Result<T, E>>,处理起来别扭。两个常用解法:
其一是 filter_map 配合 Result::ok(),把成功的值留下、失败的丢弃——适合”失败可忽略”的场景。其二是用 collect 把整个迭代器收成 Result<Collection, E>:只要链上任意一个元素是 Err,整体就短路成那个 Err;全成功才得到装好结果的集合。这个特性叫”从迭代器收集成 Result”。
let nums = ["1", "2", "x", "4"];
let sum: Result<i32, std::num::ParseIntError> =
nums.iter().map(|s| s.parse::<i32>()).sum();
// 因为 "x" 解析失败,sum 是 Err
Note
sum、collect这类消费者能直接”收集成Result”,是因为标准库为Result实现了对应的FromIterator。掌握了它,你就能在一条流水线里优雅地处理错误,而不必中途unwrap。
1-10 把整条流水线收成集合或标量
链条的终点(消费者)决定了”最后得到什么”。最常见是把结果收进集合:collect() 能收成 Vec、HashMap 等,只要类型标得出来。
let nums = [1, 2, 3, 4];
let squared: Vec<i32> = nums.iter().map(|x| x * x).collect();
// [1, 4, 9, 16]
标量结果也有一大家子:sum 求和、product 求积、count 计数、min / max 取最值、fold 自定义累积。还有 partition 这种特别的:按条件把元素分进两个集合(一个 true 桶、一个 false 桶),非常适合”分流”。
let v = [1, 2, 3, 4, 5];
let (evens, odds): (Vec<i32>, Vec<i32>) =
v.iter().copied().partition(|x| x % 2 == 0);
// evens = [2,4], odds = [1,3,5]
Tip想清楚”我最后要一个值、还是一个集合、还是两堆集合”,再选对应的消费者。选对了,整条链就干净利落地收尾,没有临时变量、没有中间数组。
1-11 链式太长怎么办:拆成有名字的函数
链式调用上头之后,容易写出一条几十个字符、三四层嵌套的”长龙”。它能跑,但隔周回来自己都看不懂。经验法则是:当一条链超过”三四个环节”或塞进了复杂闭包,就把它抽成一个有名字的函数。
比如”读取、解析、过滤、累加”这一串,与其全堆在一行,不如写成 fn total_valid(nums: &[&str]) -> Result<i32, ...>,里面每一步用一行表达,函数名本身就说明了”这条流水线在干什么”。调用方看到 total_valid(...) 就知道意图,不必读实现。
测试也更轻松:拆出来后,每个环节可以单独喂样例验证;不拆的话,你只能测整条链,定位 bug 更费劲。链式是工具不是目标——它的价值是”把意图说清楚”,一旦它开始掩盖意图,就该停下来拆函数。
Note一条好的链应当”一眼能读懂在做什么”,而不是”一眼看到尾却不懂在算啥”。可读性优先,必要时用
let给中间结果起名,或把子链提成辅助函数。
1-12 小结
- 适配器返回新迭代器,可链式串成流水线;
- 整条链是惰性的,遇到消费者(sum/collect/fold)才执行;
zip配对两迭代器,enumerate带索引,fold自定义累积;- 自己实现
Iterator只需写next,其余方法有默认实现; - 迭代器是零成本抽象,和
for循环效率相当甚至更快; - 熟练掌握适配器,能用声明式代码替代大量命令式循环。
到这里,第 32–51 章全部讲完。模块系统、集合类型、错误处理、泛型与 trait、生命周期、闭包与迭代器——这些正是你写出地道 Rust 的关键拼图。
不过,写完代码只是第一步,怎么证明它是对的?下一章我们就来讲测试:用 Rust 自带的单元测试框架,给你的函数写断言、跑校验,让”练熟”变成”真稳”。