闭包与迭代器 —— 让代码像流水线一样运转
想象一座现代化工厂:原材料从传送带一端进入,经过切割、打磨、喷涂、质检等一个个工位,最终变成成品从另一端输出。每个工位只做一件事,但串联起来就能完成极其复杂的生产任务。Rust的闭包和迭代器就是这套”流水线系统”——闭包是每个工位上的加工动作,迭代器是那条不停运转的传送带。掌握它们,你的代码会变得既简洁又高效。
📋 开篇自测:你已经知道多少?
- 你能解释闭包和普通函数的区别吗?闭包为什么能”记住”定义时的上下文变量?
Fn、FnMut、FnOnce这三个trait分别对应什么样的捕获行为?- 迭代器的”惰性求值”是什么意思?
map和collect分别扮演什么角色?- 你能说出至少三个迭代器适配器和两个消费者方法吗?
一、闭包的本质——随身携带工具箱的匿名工人
1.1 什么是闭包?
普通函数就像公司里的正式员工——它有名字,有固定的工位,只能使用公司统一配发的工具(参数)。而闭包更像一个灵活的临时工——它不一定有名字,而且可以把周围环境中的工具(变量)随手装进自己的工具箱带走使用。
fn main() {
let base_price = 100;
let tax_rate = 0.08;
// 这就是一个闭包:它"捕获"了外部的base_price和tax_rate
let calculate_total = |quantity: i32| -> f64 {
let subtotal = base_price * quantity;
subtotal as f64 * (1.0 + tax_rate)
};
println!("买3件的总价:{:.2}", calculate_total(3)); // 324.00
println!("买7件的总价:{:.2}", calculate_total(7)); // 756.00
}
闭包用|参数列表|代替(参数列表)来定义。和函数不同的是,闭包可以直接使用定义它时所在作用域的变量——base_price和tax_rate不需要作为参数传入,闭包自动”捕获”了它们。
1.2 闭包的类型推断
Rust的闭包不需要显式标注参数类型和返回类型——编译器可以从使用场景中推断出来:
fn main() {
let numbers = vec![5, 2, 8, 1, 9];
// 完整写法
let is_large_v1 = |n: &i32| -> bool { *n > 5 };
// 省略类型标注(编译器自动推断)
let is_large_v2 = |n| *n > 5;
let result: Vec<&i32> = numbers.iter().filter(is_large_v1).collect();
println!("大于5的数:{:?}", result); // [8, 9]
}
但要注意:一旦编译器根据第一次调用推断出了闭包的类型,后续调用必须使用相同的类型。闭包不像泛型函数那样可以处理多种类型。
1.3 闭包 vs 函数:对比一览
| 特性 | 普通函数 | 闭包 |
|---|---|---|
| 定义方式 | fn name(params) {} | |params| {} |
| 能否捕获环境变量 | 不能 | 能 |
| 类型标注 | 必须显式标注 | 可以省略(自动推断) |
| 可以作为参数传递 | 可以(函数指针fn) | 可以(通过Fn/FnMut/FnOnce) |
| 每个定义有唯一类型 | 否(同签名的函数类型相同) | 是(每个闭包有独一无二的匿名类型) |
🤔 想一想 为什么每个闭包都有自己的唯一类型,即使两个闭包的参数和返回值类型完全一样?(提示:不同的闭包可能捕获了不同的环境变量,它们的”工具箱”内容不同。)
二、三种闭包trait——Fn、FnMut与FnOnce
2.1 理解三种捕获方式
闭包捕获环境变量的方式有三种,分别对应三个trait。你可以把它想象成图书馆的三种借书模式:
Fn:在阅览室看书——只读,不拿走,不在上面做标记。闭包通过不可变引用(&T)捕获变量。FnMut:借回家并允许做笔记——可以修改,但要还回来。闭包通过可变引用(&mut T)捕获变量。FnOnce:直接把书买走——获得所有权,只能”消费”一次。闭包通过值(T)捕获变量。
fn main() {
// Fn:只读取捕获的变量
let greeting = String::from("你好");
let say_hi = || println!("{}", greeting);
say_hi();
say_hi(); // 可以多次调用
println!("greeting还在:{}", greeting); // 原变量仍可用
// FnMut:修改捕获的变量
let mut tally = 0;
let mut increment = || {
tally += 1;
println!("当前计数:{}", tally);
};
increment(); // 当前计数:1
increment(); // 当前计数:2
// FnOnce:消费捕获的变量
let ticket = String::from("入场券#001");
let use_ticket = || {
let consumed = ticket; // 取得所有权
println!("使用了:{}", consumed);
// ticket在这里被消费掉了
};
use_ticket();
// use_ticket(); // 编译错误!ticket已经被消费,不能再调用
}
2.2 编译器如何决定捕获方式?
编译器会根据闭包体内对变量的使用方式,自动选择最宽松的捕获方式:
只读取变量 → 不可变引用捕获(Fn)
修改变量 → 可变引用捕获(FnMut)
转移所有权 → 值捕获(FnOnce)
这三个trait之间存在继承关系——实现了Fn的闭包一定也实现了FnMut和FnOnce(因为”只读”当然可以被当作”可修改”或”可消费”来用):
FnOnce(最宽泛)
↑ 继承
FnMut
↑ 继承
Fn(最严格)
2.3 trait层级的实际含义
// 接受Fn的函数:闭包只需要读取环境
fn repeat_action<F: Fn()>(action: F, times: usize) {
for _ in 0..times {
action(); // 可以多次调用
}
}
// 接受FnMut的函数:闭包可能修改环境
fn apply_mutations<F: FnMut()>(mut action: F, times: usize) {
for _ in 0..times {
action();
}
}
// 接受FnOnce的函数:闭包可能消费环境,只能调用一次
fn execute_once<F: FnOnce() -> String>(action: F) -> String {
action()
}
fn main() {
let label = String::from("标签");
repeat_action(|| println!("打印:{}", label), 3);
let mut counter = 0;
apply_mutations(|| { counter += 1; }, 5);
println!("计数器:{}", counter); // 5
let data = vec![1, 2, 3];
let result = execute_once(|| {
let owned = data; // 消费data
format!("数据共{}项", owned.len())
});
println!("{}", result); // 数据共3项
}
三、闭包作为参数和返回值
3.1 闭包作为参数的三种方式
// 方式1:泛型 + trait bound(静态分发,最常用)
fn process_v1<F: Fn(i32) -> i32>(value: i32, transformer: F) -> i32 {
transformer(value)
}
// 方式2:impl Trait(方式1的语法糖,更简洁)
fn process_v2(value: i32, transformer: impl Fn(i32) -> i32) -> i32 {
transformer(value)
}
// 方式3:trait对象(动态分发,灵活但有运行时开销)
fn process_v3(value: i32, transformer: &dyn Fn(i32) -> i32) -> i32 {
transformer(value)
}
fn main() {
let double = |x| x * 2;
println!("{}", process_v1(5, double)); // 10
println!("{}", process_v2(5, |x| x + 100)); // 105
println!("{}", process_v3(5, &|x| x * x)); // 25
}
方式1和方式2在编译后效果完全一样(编译器为每种闭包生成专门的代码,零运行时开销)。方式3通过虚函数表在运行时动态查找,适合需要把不同闭包放在集合里的场景。
3.2 闭包作为返回值
因为每个闭包都有独一无二的匿名类型,返回闭包时需要特殊处理:
// 方式1:impl Fn(编译器知道具体类型,零开销)
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
// 方式2:Box<dyn Fn>(装箱到堆上,用于需要动态分发的场景)
fn make_operator(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
"add" => Box::new(|a, b| a + b),
"mul" => Box::new(|a, b| a * b),
_ => Box::new(|a, b| a - b),
}
}
fn main() {
let triple = make_multiplier(3);
println!("3倍:{}", triple(7)); // 21
let add = make_operator("add");
let mul = make_operator("mul");
println!("加法:{}", add(3, 4)); // 7
println!("乘法:{}", mul(3, 4)); // 12
}
impl Fn适用于函数只返回一种闭包的情况。如果函数可能返回不同的闭包(比如make_operator根据参数返回不同逻辑),就必须用Box<dyn Fn>把它们装箱。
3.3 move闭包与线程安全
move关键字强制闭包获取捕获变量的所有权,这在多线程场景中尤其重要:
use std::thread;
fn spawn_printer(label: String) -> thread::JoinHandle<()> {
// move让闭包获取label的所有权,保证线程安全
thread::spawn(move || {
for i in 1..=3 {
println!("[{}] 第{}次打印", label, i);
}
})
}
fn main() {
let h1 = spawn_printer(String::from("线程A"));
let h2 = spawn_printer(String::from("线程B"));
h1.join().unwrap();
h2.join().unwrap();
}
为什么要move?因为子线程可能比创建它的函数活得更久。如果闭包只是借用label,而函数返回后label就被销毁了,子线程就会持有一个悬垂引用。move把所有权转移进闭包,彻底消除了这个风险。
🤔 想一想 如果一个闭包捕获的变量实现了
Copytrait(比如i32),move会怎样?(提示:Copy类型的”move”实际上是复制一份,原变量依然可用。)
四、Iterator trait——传送带的核心引擎
4.1 Iterator trait长什么样?
Rust标准库中Iterator trait的核心定义非常简洁:
trait Iterator {
type Item; // 关联类型:迭代器产出的元素类型
fn next(&mut self) -> Option<Self::Item>;
// ... 以下几十个方法都有默认实现,不需要手写
}
整个迭代器系统只有一个核心方法:next()。每次调用它,返回Some(下一个元素)或None(没有更多元素了)。这就像传送带上的传感器——每次读取一个物品,读到空位就表示结束。
fn main() {
let flavors = vec!["香草", "巧克力", "抹茶"];
let mut iter = flavors.iter();
println!("{:?}", iter.next()); // Some("香草")
println!("{:?}", iter.next()); // Some("巧克力")
println!("{:?}", iter.next()); // Some("抹茶")
println!("{:?}", iter.next()); // None
}
4.2 三种迭代方式
对于一个集合,有三种方式创建迭代器:
fn main() {
let cities = vec![
String::from("北京"),
String::from("上海"),
String::from("深圳"),
];
// iter():借用元素(&T),集合本身不受影响
for city in cities.iter() {
println!("参观:{}", city); // city的类型是&String
}
println!("cities还在:{:?}", cities);
// iter_mut():可变借用元素(&mut T),可以修改
let mut scores = vec![60, 70, 80];
for score in scores.iter_mut() {
*score += 10; // 每个成绩加10分
}
println!("加分后:{:?}", scores); // [70, 80, 90]
// into_iter():消费集合,获取元素所有权(T)
let names = vec![String::from("Alice"), String::from("Bob")];
for name in names.into_iter() {
println!("欢迎:{}", name); // name的类型是String
}
// println!("{:?}", names); // 编译错误!names已被消费
}
| 方法 | 元素类型 | 集合是否可用 | 适用场景 |
|---|---|---|---|
.iter() | &T | 集合不受影响 | 只需读取数据 |
.iter_mut() | &mut T | 元素被修改 | 需要就地修改 |
.into_iter() | T | 集合被消费 | 需要转移所有权 |
for item in &collection等价于for item in collection.iter(),for item in collection等价于for item in collection.into_iter()。
五、迭代器适配器——流水线上的加工工位
迭代器适配器是那些接收一个迭代器、返回另一个迭代器的方法。它们像流水线上的工位,可以自由拼接组合。
5.1 核心适配器一览
fn main() {
let readings = vec![3, 7, 2, 9, 4, 6, 1, 8, 5];
// map:对每个元素施加变换——"喷涂工位"
let doubled: Vec<i32> = readings.iter().map(|x| x * 2).collect();
println!("翻倍:{:?}", doubled); // [6, 14, 4, 18, 8, 12, 2, 16, 10]
// filter:筛选满足条件的元素——"质检工位"
// 注意双重解引用:iter()产生&i32,filter的闭包参数是&&i32的引用,
// 所以需要**x先解引用到&i32,再解引用到i32才能与5比较
let big: Vec<&i32> = readings.iter().filter(|x| **x > 5).collect();
println!("大于5:{:?}", big); // [7, 9, 6, 8]
// take:只取前N个元素——"限量生产"
let first_three: Vec<&i32> = readings.iter().take(3).collect();
println!("前3个:{:?}", first_three); // [3, 7, 2]
// skip:跳过前N个元素——"跳过预热阶段"
let after_skip: Vec<&i32> = readings.iter().skip(6).collect();
println!("跳过6个:{:?}", after_skip); // [1, 8, 5]
// enumerate:附带索引——"给每件产品编号"
for (idx, val) in readings.iter().enumerate().take(4) {
println!(" 第{}号:{}", idx, val);
}
// zip:把两个迭代器配对——"合并两条流水线"
let labels = vec!["甲", "乙", "丙"];
let values = vec![100, 200, 300];
let pairs: Vec<(&&str, &i32)> = labels.iter().zip(values.iter()).collect();
println!("配对:{:?}", pairs); // [("甲", 100), ("乙", 200), ("丙", 300)]
}
5.2 flat_map——展平嵌套结构
flat_map先对每个元素做映射(得到一个迭代器),然后把所有结果展平成一层:
fn main() {
let sentences = vec!["Rust很快", "也很安全", "值得学习"];
// 把每个句子拆成字符,然后展平成一个字符序列
let all_chars: Vec<char> = sentences
.iter()
.flat_map(|s| s.chars())
.collect();
println!("所有字符:{:?}", all_chars);
// 更实用的例子:提取嵌套数组中的所有元素
let matrix = vec![vec![1, 2, 3], vec![4, 5], vec![6, 7, 8, 9]];
let flat: Vec<&i32> = matrix.iter().flat_map(|row| row.iter()).collect();
println!("展平矩阵:{:?}", flat); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
5.3 链式调用——流水线的威力
多个适配器可以链在一起,形成一条完整的数据处理流水线:
fn main() {
let raw_inputs = vec![" 42 ", "hello", " 7", "world", "13", "", "99"];
// 从原始字符串中提取有效数字,翻倍后保留大于20的
let results: Vec<i32> = raw_inputs
.iter()
.map(|s| s.trim()) // 去除空白
.filter(|s| !s.is_empty()) // 过滤空字符串
.filter_map(|s| s.parse::<i32>().ok()) // 尝试解析为数字,忽略失败的
.map(|n| n * 2) // 翻倍
.filter(|n| *n > 20) // 保留大于20的
.collect(); // 收集结果
println!("处理结果:{:?}", results); // [84, 26, 198]
}
这段代码如果用传统循环写,至少需要一个可变的Vec和多层嵌套的if判断。迭代器链让数据处理逻辑变成了一条清晰的管道——每一步做什么一目了然。
六、消费者方法——流水线的终点站
适配器只是”配置”流水线上的工位,并不会真正启动生产。消费者方法才是按下”启动”按钮的那个动作——它会驱动整条迭代器链执行。
6.1 常用消费者
fn main() {
let expenses = vec![120, 45, 300, 78, 210, 15, 99];
// collect:收集成集合(最常用的消费者)
let expensive: Vec<&i32> = expenses.iter().filter(|x| **x > 100).collect();
println!("大额支出:{:?}", expensive); // [120, 300, 210]
// sum:求和
let total: i32 = expenses.iter().sum();
println!("总支出:{}", total); // 867
// fold:折叠——最通用的聚合操作
let total_with_tax = expenses.iter().fold(0.0_f64, |acc, &x| {
acc + x as f64 * 1.1 // 每笔支出加10%税
});
println!("含税总计:{:.2}", total_with_tax); // 953.70
// any:是否存在满足条件的元素
let has_big_expense = expenses.iter().any(|x| *x > 200);
println!("有超过200的支出?{}", has_big_expense); // true
// all:是否所有元素都满足条件
let all_positive = expenses.iter().all(|x| *x > 0);
println!("全部为正数?{}", all_positive); // true
// find:找到第一个满足条件的元素
let first_big = expenses.iter().find(|x| **x > 200);
println!("第一笔大额:{:?}", first_big); // Some(300)
// count:计数
let small_count = expenses.iter().filter(|x| **x < 50).count();
println!("小额笔数:{}", small_count); // 1
// min / max
println!("最小支出:{:?}", expenses.iter().min()); // Some(15)
println!("最大支出:{:?}", expenses.iter().max()); // Some(300)
}
6.2 fold详解——万能的”折叠机”
fold是所有聚合操作的祖宗——sum、count、any、all本质上都可以用fold来实现:
fn main() {
let words = vec!["Rust", "是", "一门", "系统", "编程", "语言"];
// 用fold拼接字符串
let sentence = words.iter().fold(String::new(), |mut acc, &w| {
if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(w);
acc
});
println!("{}", sentence); // Rust 是 一门 系统 编程 语言
// 用fold统计字符频率
let text = "abracadabra";
let freq = text.chars().fold(std::collections::HashMap::new(), |mut map, ch| {
*map.entry(ch).or_insert(0) += 1;
map
});
println!("字符频率:{:?}", freq); // {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
}
fold的参数是:初始值(累加器的起点)和一个闭包(接收累加器和当前元素,返回新的累加器)。它从左到右”折叠”整个序列,最终得到一个值。
七、惰性求值——不发动就不耗油
7.1 适配器是”懒”的
Rust的迭代器适配器采用惰性求值——调用map、filter这些方法时,不会立即处理数据,只是”记录”了一个处理步骤。直到消费者方法(collect、sum等)被调用时,整条链才开始一个元素一个元素地执行。
fn main() {
let data = vec![1, 2, 3, 4, 5];
// 这一行什么都不会执行!只是构建了处理计划
let lazy_chain = data.iter()
.map(|x| {
println!(" map处理:{}", x);
x * 10
})
.filter(|x| {
println!(" filter检查:{}", x);
*x > 20
});
println!("--- 链已构建,但还没执行 ---");
println!("--- 现在调用collect触发执行 ---");
let result: Vec<i32> = lazy_chain.collect();
println!("结果:{:?}", result);
}
运行输出:
--- 链已构建,但还没执行 ---
--- 现在调用collect触发执行 ---
map处理:1
filter检查:10
map处理:2
filter检查:20
map处理:3
filter检查:30
map处理:4
filter检查:40
map处理:5
filter检查:50
结果:[30, 40, 50]
注意输出顺序:不是先把所有元素map完再filter,而是逐个元素依次通过map和filter。元素1先经过map变成10,再经过filter被淘汰;然后元素2经过map变成20,再经过filter被淘汰……以此类推。这就像真正的流水线——每个产品逐个通过所有工位。
7.2 惰性求值的好处
fn main() {
// 从一亿个数中找到第一个满足条件的——惰性求值只处理必要的元素
let result = (0..100_000_000)
.map(|x| x * 3)
.filter(|x| x % 7 == 0)
.find(|x| *x > 1000);
println!("结果:{:?}", result); // Some(1008)
// 实际只处理了几百个元素,而不是一亿个!
}
如果不是惰性求值,你需要先生成一亿个数,再全部乘以3,再全部过滤——光内存就爆了。惰性求值让迭代器链可以处理无限序列或超大数据集,因为它每次只处理一个元素。
八、自定义迭代器——打造你自己的传送带
8.1 为自定义类型实现Iterator
实现Iterator trait只需要定义Item类型和next()方法:
struct Countdown {
remaining: u32,
}
impl Countdown {
fn new(start: u32) -> Self {
Countdown { remaining: start }
}
}
impl Iterator for Countdown {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
None // 倒计时结束
} else {
let current = self.remaining;
self.remaining -= 1;
Some(current)
}
}
}
fn main() {
let countdown = Countdown::new(5);
// 自定义迭代器自动获得所有适配器和消费者方法!
let result: Vec<u32> = countdown
.filter(|x| x % 2 == 1) // 只保留奇数
.map(|x| x * 100) // 乘以100
.collect();
println!("{:?}", result); // [500, 300, 100]
}
只实现了一个next()方法,就免费获得了map、filter、fold、collect等几十个方法——这就是trait默认实现的威力。
8.2 实战:为自定义集合实现迭代器
来看一个更实际的例子——为环形缓冲区实现迭代器:
struct RingBuffer<T> {
data: Vec<T>,
capacity: usize,
}
impl<T> RingBuffer<T> {
fn new(capacity: usize) -> Self {
RingBuffer {
data: Vec::with_capacity(capacity),
capacity,
}
}
fn push(&mut self, item: T) {
if self.data.len() >= self.capacity {
self.data.remove(0); // 移除最旧的元素(O(n)操作,生产环境应使用 VecDeque)
}
self.data.push(item);
}
fn iter(&self) -> RingBufferIter<T> {
RingBufferIter {
buffer: &self.data,
index: 0,
}
}
}
struct RingBufferIter<'a, T> {
buffer: &'a [T],
index: usize,
}
impl<'a, T> Iterator for RingBufferIter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if self.index < self.buffer.len() {
let item = &self.buffer[self.index];
self.index += 1;
Some(item)
} else {
None
}
}
}
fn main() {
let mut buf = RingBuffer::new(4);
for i in 1..=7 {
buf.push(i);
}
// 缓冲区容量为4,只保留最近的4个元素:4, 5, 6, 7
let contents: Vec<&i32> = buf.iter().collect();
println!("缓冲区内容:{:?}", contents); // [4, 5, 6, 7]
let sum: i32 = buf.iter().sum();
println!("缓冲区总和:{}", sum); // 22
let evens: Vec<&&i32> = buf.iter().filter(|x| **x % 2 == 0).collect();
println!("偶数元素:{:?}", evens); // [4, 6]
}
自定义迭代器的关键是:实现Iterator trait后,标准库提供的几十个适配器和消费者方法全部自动可用。
九、迭代器 vs 循环——零成本抽象的证明
9.1 性能对比
很多人直觉认为”迭代器链这么花哨,性能一定不如朴素的for循环”。事实恰恰相反:
// 传统循环方式
fn sum_of_squares_loop(data: &[i64]) -> i64 {
let mut total = 0i64;
for &val in data {
if val > 0 {
total += val * val;
}
}
total
}
// 迭代器方式
fn sum_of_squares_iter(data: &[i64]) -> i64 {
data.iter()
.filter(|&&v| v > 0)
.map(|&v| v * v)
.sum()
}
在cargo build --release编译后,这两个函数生成的机器码几乎完全相同。编译器会把迭代器链”展开”成等价的循环代码——这就是Rust所说的零成本抽象(zero-cost abstraction)。
用一幅图来表示:
你写的代码 编译后的机器码
┌──────────────────┐ ┌──────────────────┐
│ .iter() │ │ │
│ .filter(...) │ 编译优化 │ 一个高效的 │
│ .map(...) │ ─────────→ │ 展开循环 │
│ .sum() │ │ (等价于手写) │
└──────────────────┘ └──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ for val in data │ │ │
│ if val > 0 │ 编译优化 │ 同样的 │
│ total += │ ─────────→ │ 展开循环 │
│ val*val │ │ │
└──────────────────┘ └──────────────────┘
9.2 何时该用哪种风格?
| 场景 | 推荐风格 | 理由 |
|---|---|---|
| 简单遍历 | 都可以 | for循环更直观 |
| 数据变换管道 | 迭代器链 | 逻辑更清晰,不需要中间变量 |
| 需要提前break | for循环 | 迭代器链不方便提前退出 |
| 复杂条件筛选 | 迭代器链 | filter_map/take_while比嵌套if优雅 |
| 需要索引操作 | for循环 | enumerate虽然行,但有时不如直接用索引 |
| 函数式组合 | 迭代器链 | 闭包+适配器可以像搭积木一样组合 |
十、实战:用迭代器链分析服务器日志
来看一个真实场景——分析Web服务器日志,找出响应最慢的接口:
use std::collections::HashMap;
#[derive(Debug)]
struct LogEntry {
timestamp: String,
method: String,
path: String,
status: u16,
duration_ms: u64,
}
impl LogEntry {
fn new(timestamp: &str, method: &str, path: &str, status: u16, duration_ms: u64) -> Self {
LogEntry {
timestamp: timestamp.to_string(),
method: method.to_string(),
path: path.to_string(),
status,
duration_ms,
}
}
}
fn main() {
// 模拟日志数据
let logs = vec![
LogEntry::new("10:01:05", "GET", "/api/users", 200, 45),
LogEntry::new("10:01:06", "POST", "/api/orders", 201, 320),
LogEntry::new("10:01:07", "GET", "/api/users", 200, 52),
LogEntry::new("10:01:08", "GET", "/api/products", 200, 18),
LogEntry::new("10:01:09", "POST", "/api/orders", 500, 1500),
LogEntry::new("10:01:10", "GET", "/api/users", 200, 38),
LogEntry::new("10:01:11", "GET", "/api/products", 200, 22),
LogEntry::new("10:01:12", "POST", "/api/orders", 201, 280),
LogEntry::new("10:01:13", "GET", "/api/products", 404, 5),
LogEntry::new("10:01:14", "GET", "/api/users", 200, 41),
];
// 1. 统计错误请求(状态码 >= 400)
let error_count = logs.iter()
.filter(|entry| entry.status >= 400)
.count();
println!("错误请求数:{}", error_count);
// 2. 找出最慢的3个请求
let mut durations: Vec<(&LogEntry)> = logs.iter().collect();
durations.sort_by(|a, b| b.duration_ms.cmp(&a.duration_ms));
println!("\n最慢的3个请求:");
for entry in durations.iter().take(3) {
println!(" {} {} → {}ms (状态{})",
entry.method, entry.path, entry.duration_ms, entry.status);
}
// 3. 按路径分组,计算每个路径的平均响应时间
let path_stats: HashMap<&str, (u64, u64)> = logs.iter()
.fold(HashMap::new(), |mut acc, entry| {
let stat = acc.entry(entry.path.as_str()).or_insert((0, 0));
stat.0 += entry.duration_ms; // 总耗时
stat.1 += 1; // 请求次数
acc
});
println!("\n各路径平均响应时间:");
let mut path_avgs: Vec<(&str, f64)> = path_stats.iter()
.map(|(&path, &(total, count))| (path, total as f64 / count as f64))
.collect();
path_avgs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
for (path, avg) in &path_avgs {
println!(" {} → {:.1}ms", path, avg);
}
// 4. 找出所有POST请求中成功的(2xx),提取路径
let successful_posts: Vec<&str> = logs.iter()
.filter(|e| e.method == "POST")
.filter(|e| e.status >= 200 && e.status < 300)
.map(|e| e.path.as_str())
.collect();
println!("\n成功的POST请求路径:{:?}", successful_posts);
// 5. 是否所有GET请求都在100ms内完成?
let all_gets_fast = logs.iter()
.filter(|e| e.method == "GET")
.all(|e| e.duration_ms < 100);
println!("\n所有GET请求都在100ms内?{}", all_gets_fast);
}
这个例子展示了迭代器在真实数据处理中的威力。每一步操作都清晰可读:filter筛选、map变换、fold聚合、sort_by排序——整个分析过程像在搭建一条数据处理流水线。
十一、实战:用闭包和迭代器构建查询构建器
最后来做一个综合练习——实现一个支持链式调用的查询构建器,模拟数据库查询的构建过程:
#[derive(Debug, Clone)]
struct Employee {
name: String,
department: String,
salary: u32,
years: u32,
}
struct Query<'a> {
source: &'a [Employee],
filters: Vec<Box<dyn Fn(&Employee) -> bool + 'a>>,
sorter: Option<Box<dyn Fn(&Employee, &Employee) -> std::cmp::Ordering + 'a>>,
limit: Option<usize>,
}
impl<'a> Query<'a> {
fn new(source: &'a [Employee]) -> Self {
Query {
source,
filters: Vec::new(),
sorter: None,
limit: None,
}
}
// 添加过滤条件——接受闭包作为参数
fn where_clause<F>(mut self, predicate: F) -> Self
where
F: Fn(&Employee) -> bool + 'a,
{
self.filters.push(Box::new(predicate));
self
}
// 设置排序规则
fn order_by<F>(mut self, compare: F) -> Self
where
F: Fn(&Employee, &Employee) -> std::cmp::Ordering + 'a,
{
self.sorter = Some(Box::new(compare));
self
}
// 限制返回数量
fn take(mut self, n: usize) -> Self {
self.limit = Some(n);
self
}
// 执行查询——消费Query,返回结果
fn execute(self) -> Vec<Employee> {
// 用迭代器链应用所有过滤器
let mut results: Vec<Employee> = self.source.iter()
.filter(|emp| {
self.filters.iter().all(|f| f(emp))
})
.cloned()
.collect();
// 应用排序
if let Some(sorter) = &self.sorter {
results.sort_by(|a, b| sorter(a, b));
}
// 应用数量限制
if let Some(limit) = self.limit {
results.truncate(limit);
}
results
}
}
fn main() {
let staff = vec![
Employee { name: "张伟".into(), department: "工程".into(), salary: 25000, years: 5 },
Employee { name: "李娜".into(), department: "设计".into(), salary: 20000, years: 3 },
Employee { name: "王芳".into(), department: "工程".into(), salary: 30000, years: 8 },
Employee { name: "赵强".into(), department: "市场".into(), salary: 18000, years: 2 },
Employee { name: "孙丽".into(), department: "工程".into(), salary: 28000, years: 6 },
Employee { name: "周杰".into(), department: "设计".into(), salary: 22000, years: 4 },
Employee { name: "吴敏".into(), department: "市场".into(), salary: 19000, years: 3 },
Employee { name: "陈刚".into(), department: "工程".into(), salary: 35000, years: 10 },
];
// 链式查询:工程部、薪资>25000、按工龄降序排列、取前2名
let top_engineers = Query::new(&staff)
.where_clause(|e| e.department == "工程")
.where_clause(|e| e.salary > 25000)
.order_by(|a, b| b.years.cmp(&a.years))
.take(2)
.execute();
println!("高薪资深工程师:");
for emp in &top_engineers {
println!(" {} - 工龄{}年 - 月薪{}",
emp.name, emp.years, emp.salary);
}
// 另一个查询:所有工龄>=4年的员工,按薪资升序
let experienced = Query::new(&staff)
.where_clause(|e| e.years >= 4)
.order_by(|a, b| a.salary.cmp(&b.salary))
.execute();
println!("\n资深员工(按薪资排序):");
for emp in &experienced {
println!(" {} ({}) - 工龄{}年 - ¥{}",
emp.name, emp.department, emp.years, emp.salary);
}
// 统计查询:各部门的平均薪资
let departments: Vec<&str> = staff.iter()
.map(|e| e.department.as_str())
.collect::<std::collections::HashSet<&str>>()
.into_iter()
.collect();
println!("\n各部门平均薪资:");
for dept in &departments {
let (total, count) = staff.iter()
.filter(|e| e.department == *dept)
.fold((0u64, 0u64), |(sum, cnt), e| (sum + e.salary as u64, cnt + 1));
println!(" {} → ¥{:.0}", dept, total as f64 / count as f64);
}
}
这个查询构建器展示了闭包与迭代器的完美配合:
- 闭包让
where_clause和order_by可以接受任意过滤和排序逻辑 Box<dyn Fn>让不同的闭包可以存放在同一个Vec中- 迭代器链让
execute方法内部的数据处理清晰简洁 - 链式调用(每个方法返回
Self)让使用者的代码读起来像自然语言
⚠️ 常见误区
- 忘了迭代器是惰性的——
v.iter().map(|x| x * 2)什么都不会做,必须调用collect()或其他消费者方法才会执行。如果你发现map里的println!没有输出,多半是忘了消费。- 混淆
iter()、into_iter()、iter_mut()——它们分别产生&T、T、&mut T。在filter中用iter()时,闭包参数是&&T(引用的引用),这经常让初学者困惑。- 在闭包中意外夺取所有权——闭包默认用最小权限捕获变量。如果你在闭包中写了
let x = some_string而不是let x = &some_string,会触发所有权转移,导致闭包变成FnOnce。- 对
Fntrait边界选择过严——函数参数尽量用FnOnce(最宽泛),除非你确实需要多次调用闭包。Fn是最严格的要求,会限制调用者能传入的闭包种类。- 以为迭代器比循环慢——在release模式下,迭代器链和手写循环的性能几乎没有差别。性能问题更多出在算法选择上,而不是编程风格上。
📝 掌握度自测
-
闭包基础:写一个函数
apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32,对x连续应用两次f。用它计算apply_twice(|n| n + 3, 10)的结果。 -
三种Fn trait:解释以下代码为什么无法编译,并修复它:
fn call_many_times<F: FnOnce()>(f: F) { f(); f(); // 为什么这里报错? } -
迭代器链:给定
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],用一条迭代器链找出所有偶数,计算它们的平方,然后求和。写出代码并说出结果。 -
自定义迭代器:实现一个
Fibonacci结构体,让它实现Iteratortrait,可以无限产出斐波那契数列。然后用.take(10).collect::<Vec<u64>>()获取前10个数。 -
综合应用:给定一组字符串
vec!["hello world", "rust is great", "iterators rock"],用迭代器链完成:把每个字符串按空格拆分成单词,展平成一个单词列表,过滤掉长度小于4的单词,全部转为大写,然后收集成Vec<String>。
💡 自我评估
- 答对5题:闭包和迭代器已融会贯通,你可以用函数式风格写出优雅高效的Rust代码了。
- 答对3-4题:核心概念已掌握,建议多练习迭代器链的组合使用和自定义迭代器的实现。
- 答对0-2题:建议从简单的闭包和
map/filter/collect三件套开始,逐步增加复杂度。理解惰性求值是掌握迭代器的关键转折点。
购买课程解锁全部内容
内存安全 + 零成本抽象:Rust 系统编程实战
¥29.90