本篇导读
今天是第一卷的收官篇。我们学习 Rust 函数式编程风格的两大工具,它们也是上一篇重构 minigrep 的“秘密武器”:
- 闭包(Closure):一种可以保存到变量、作为参数传递,并且能捕获其定义环境的匿名函数;
- 迭代器(Iterator):对一系列元素进行遍历的统一抽象,配合
map、filter、fold 等适配器,用“声明要什么”的方式处理数据,且全部是零成本抽象。
本篇你将学会:闭包的语法与类型推断、闭包如何捕获环境(Fn / FnMut / FnOnce 三 trait)、迭代器的 Iterator trait 与 next、常用适配器与消费者,以及如何让自己的类型可迭代。
一、闭包:捕获环境的匿名函数
1. 闭包的语法
闭包用 |参数| 表达式 定义,类似其他语言的 lambda:
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
// 多参数、带类型标注、带代码块
let add = |x: i32, y: i32| -> i32 { x + y };
println!("{}", add(2, 3)); // 5
与函数对比:
fn add_v1(x: i32) -> i32 { x + 1 } // 函数
let add_v2 = |x: i32| -> i32 { x + 1 }; // 闭包
2. 闭包与函数的关键区别:能捕获环境
普通函数不能使用其作用域外的局部变量,而闭包可以:
fn main() {
let factor = 10;
let multiply = |x| x * factor; // 捕获了外部的 factor
println!("{}", multiply(5)); // 50
}
3. 闭包的类型推断
闭包不要求你标注参数和返回值类型——编译器会从首次调用推断出一个固定的具体类型。这与函数“必须标注”不同。当然你也可以显式标注以提高清晰度。注意一旦推断确定,就不能用不同类型调用同一个闭包。
4. 一个实用场景:缓存/延迟计算
闭包常用来表示“稍后才执行的计算”。标准库的 Iterator::map、线程 thread::spawn、各种 builder 都接收闭包。
二、闭包捕获环境的三种方式:Fn / FnMut / FnOnce
闭包捕获环境变量的方式,由它如何使用这些变量决定,对应三个 trait,这是闭包部分最核心的知识点:
1. FnOnce:消耗捕获的值
如果闭包把捕获到的值移动进自己内部(甚至返回它),它只能被调用一次,称为 FnOnce。加 move 关键字会强制获取所有权:
let name = String::from("小明");
let consume = move || {
println!("消耗了:{name}"); // name 被移动进闭包
};
consume();
// name 已不可用
2. FnMut:可变地借用环境
闭包修改捕获的变量,就是 FnMut,可多次调用:
let mut count = 0;
let mut increment = || {
count += 1; // 可变借用 count
println!("count = {count}");
};
increment();
increment();
3. Fn:只读地借用环境
闭包只读取捕获变量、不修改,就是 Fn,可任意多次调用:
let factor = 3;
let times = |x| x * factor; // 只读借用 factor
println!("{} {}", times(2), times(3));
三者关系
- 一个闭包实现哪个 trait,取决于它对捕获值的使用方式,编译器自动判定;
Fn(只读)最宽松、FnMut 次之、FnOnce 最严格;所有 Fn 都满足 FnMut 和 FnOnce;
- 当函数要接收闭包参数时,按需要选择约束:只读多次用
Fn、要修改用 FnMut、只调用一次用 FnOnce。
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
三、迭代器:统一的遍历抽象
1. Iterator trait 与 next
所有迭代器都实现标准库的 Iterator trait,其核心是 next 方法:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...大量默认方法
}
next 每次返回 Some(下一个元素),耗尽后返回 None;
type Item 是关联类型(第 37 篇讲);
- 用
for 遍历迭代器时,Rust 自动反复调用 next。
手动调用 next 看看:
let v = vec![1, 2, 3];
let mut iter = v.iter();
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);
2. 三种常见迭代方式
let v = vec![1, 2, 3];
v.iter(); // 产生 &T(只读引用)
let mut v = vec![1, 2, 3];
v.iter_mut(); // 产生 &mut T
v.into_iter(); // 产生 T,获取所有权、原集合被消耗
四、迭代器适配器与消费者
迭代器方法分两类:适配器(adaptors) 惰性地产生另一个迭代器;消费者(consumers) 真正触发迭代、产生结果。
1. 惰性:适配器不调用就不执行
let v = vec![1, 2, 3];
v.iter().map(|x| x + 1); // 警告:unused Map,闭包根本没执行!
2. 常用适配器
let v = vec![1, 2, 3, 4, 5];
let mapped: Vec<_> = v.iter().map(|x| x * 2).collect(); // [2, 4, 6, 8, 10]
let filtered: Vec<_> = v.iter().filter(|&&x| x > 2).collect(); // [3, 4, 5]
let taken: Vec<_> = v.iter().take(3).collect(); // [1, 2, 3]
let skipped: Vec<_> = v.iter().skip(2).collect(); // [3, 4, 5]
let zipped: Vec<_> = vec![1, 2].iter().zip(vec!['a', 'b']).collect(); // [(1, 'a'), (2, 'b')]
let enumerated: Vec<_> = v.iter().enumerate().collect(); // 带下标
链式组合:
let result: Vec<i32> = v
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * 10)
.collect(); // [20, 40]
3. 常用消费者
let v = vec![1, 2, 3, 4, 5];
v.iter().sum::<i32>(); // 求和:15
v.iter().product::<i32>(); // 求积:120
v.iter().count(); // 计数:5
v.iter().next(); // 第一个
v.iter().max();
v.iter().min(); // 最大/最小
v.iter().any(|&x| x > 4); // 是否存在满足条件
v.iter().all(|&x| x > 0); // 是否全部满足
v.iter().position(|&x| x == 3); // 找到的下标
v.iter().for_each(|&x| println!("{x}")); // 类似 for 循环
4. fold:最通用的折叠
fold(其他语言的 reduce)用一个累加器反复组合元素,很多方法都能用它表达:
let sum = vec![1, 2, 3, 4, 5].iter().fold(0, |acc, &x| acc + x); // 15
// 统计单词出现次数
let text = "a b a c a";
let mut counts = std::collections::HashMap::new();
text.split_whitespace().for_each(|w| {
*counts.entry(w).or_insert(0) += 1;
});
五、迭代器也是零成本抽象
你可能担心 map、filter、fold 一层层闭包会比手写循环慢。事实恰恰相反:Rust 会把整条迭代器链内联、编译成和手写循环几乎一样的机器码,没有任何运行时抽象开销。官方甚至展示过迭代器版本比部分手写 C 循环更快——因为编译器更清楚边界。
高级的表达 + 底层的性能同时成立,这是 Rust 最迷人的地方。
六、让自己的类型可迭代
为自定义集合实现 Iterator,就能直接用 for 和全部适配器:
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 sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum();
println!("{sum}");
for n in Counter::new() {
println!("{n}");
}
}
只需实现一个 next,Iterator trait 上几十个方法全部免费获得——这就是 trait + 默认方法的威力。
七、综合示例:用迭代器处理文本数据
struct Employee {
name: String,
department: String,
salary: f64,
}
fn main() {
let staff = vec![
Employee { name: "小明".into(), department: "工程".into(), salary: 12000.0 },
Employee { name: "小红".into(), department: "工程".into(), salary: 15000.0 },
Employee { name: "小刚".into(), department: "销售".into(), salary: 9000.0 },
];
// 工程部总薪资
let engineering_payroll: f64 = staff
.iter()
.filter(|e| e.department == "工程")
.map(|e| e.salary)
.sum();
println!("工程部总薪资:{engineering_payroll}");
// 所有员工姓名
let names: Vec<String> = staff
.iter()
.map(|e| e.name.clone())
.collect();
println!("员工:{names:?}");
// 最高薪
let top = staff.iter().max_by(|a, b| a.salary.partial_cmp(&b.salary).unwrap());
if let Some(e) = top {
println!("最高薪:{} ({})", e.name, e.salary);
}
}
这展示了真实数据处理中 filter / map / sum / max_by 的组合方式:声明式、清晰、零开销。
八、本篇小结
- 闭包是
|参数| 表达式 形式的匿名函数,能捕获定义环境;类型可由首次调用推断;
- 捕获三 trait:只读借用为
Fn、可变借用为 FnMut、移动消耗为 FnOnce,按需作为参数约束;move 强制获取所有权;
- 迭代器实现
Iterator trait,核心是 next 返回 Option<Item>;iter() 出引用、iter_mut() 出可变引用、into_iter() 出所有权;
- 适配器(map / filter / take / skip / zip / enumerate)惰性,消费者(collect / sum / product / count / max / min / any / all / fold / for_each)触发执行;
- 迭代器链经内联编译,是零成本抽象;
- 为自定义类型实现
next 即可获得全部迭代器能力。
九、第一卷结语
恭喜你!到这里,第一卷“筑基”全部完成。你已经掌握了:变量与类型、所有权/借用/切片、结构体/枚举、模块系统、集合、错误处理、泛型/trait/生命周期、测试,以及闭包与迭代器,并完成了 minigrep 实战。
你已经具备了独立阅读和编写中等规模 Rust 程序的能力。 从下一篇开始进入第二卷“登堂”,我们将探索智能指针、无畏并发、宏与异步——那些让 Rust 真正强大、并区别于一切其他语言的进阶主题。
十、动手练习
- 写一个闭包捕获一个
Vec<i32> 并对其求和(分别尝试 Fn 与 move 版本,观察所有权差异);
- 用迭代器重写第 3 篇的素数打印:
(2..n).filter(|x| ...).collect();
- 用
fold 实现一个函数,求切片的最大值(不使用内置 max);
- 实现一个从
start 到 end 的范围迭代器结构体;
- (选做)用闭包 + 迭代器实现一个简单的“单词频率”函数,并按频率从高到低排序输出。
下篇预告:第 16 篇|智能指针(上):Box 与 Deref
第二卷开始!我们将深入 Rust 的指针世界:什么是智能指针、Box<T> 如何在堆上分配值、Deref trait 如何让自定义指针像普通引用一样使用、Drop trait 如何控制资源清理。
我们还会用 Box 实现递归类型(如链表)。下篇见。