跳到主要内容
预计阅读 66 分钟

闭包与迭代器 —— 让代码像流水线一样运转

想象一座现代化工厂:原材料从传送带一端进入,经过切割、打磨、喷涂、质检等一个个工位,最终变成成品从另一端输出。每个工位只做一件事,但串联起来就能完成极其复杂的生产任务。Rust的闭包和迭代器就是这套”流水线系统”——闭包是每个工位上的加工动作,迭代器是那条不停运转的传送带。掌握它们,你的代码会变得既简洁又高效。

📋 开篇自测:你已经知道多少?

  1. 你能解释闭包和普通函数的区别吗?闭包为什么能”记住”定义时的上下文变量?
  2. FnFnMutFnOnce这三个trait分别对应什么样的捕获行为?
  3. 迭代器的”惰性求值”是什么意思?mapcollect分别扮演什么角色?
  4. 你能说出至少三个迭代器适配器和两个消费者方法吗?

一、闭包的本质——随身携带工具箱的匿名工人

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_pricetax_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的闭包一定也实现了FnMutFnOnce(因为”只读”当然可以被当作”可修改”或”可消费”来用):

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把所有权转移进闭包,彻底消除了这个风险。

🤔 想一想 如果一个闭包捕获的变量实现了Copy trait(比如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是所有聚合操作的祖宗——sumcountanyall本质上都可以用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的迭代器适配器采用惰性求值——调用mapfilter这些方法时,不会立即处理数据,只是”记录”了一个处理步骤。直到消费者方法(collectsum等)被调用时,整条链才开始一个元素一个元素地执行。

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()方法,就免费获得了mapfilterfoldcollect等几十个方法——这就是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循环更直观
数据变换管道迭代器链逻辑更清晰,不需要中间变量
需要提前breakfor循环迭代器链不方便提前退出
复杂条件筛选迭代器链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_clauseorder_by可以接受任意过滤和排序逻辑
  • Box<dyn Fn>让不同的闭包可以存放在同一个Vec
  • 迭代器链让execute方法内部的数据处理清晰简洁
  • 链式调用(每个方法返回Self)让使用者的代码读起来像自然语言

⚠️ 常见误区

  1. 忘了迭代器是惰性的——v.iter().map(|x| x * 2)什么都不会做,必须调用collect()或其他消费者方法才会执行。如果你发现map里的println!没有输出,多半是忘了消费。
  2. 混淆iter()into_iter()iter_mut()——它们分别产生&TT&mut T。在filter中用iter()时,闭包参数是&&T(引用的引用),这经常让初学者困惑。
  3. 在闭包中意外夺取所有权——闭包默认用最小权限捕获变量。如果你在闭包中写了let x = some_string而不是let x = &some_string,会触发所有权转移,导致闭包变成FnOnce
  4. Fn trait边界选择过严——函数参数尽量用FnOnce(最宽泛),除非你确实需要多次调用闭包。Fn是最严格的要求,会限制调用者能传入的闭包种类。
  5. 以为迭代器比循环慢——在release模式下,迭代器链和手写循环的性能几乎没有差别。性能问题更多出在算法选择上,而不是编程风格上。

📝 掌握度自测

  1. 闭包基础:写一个函数apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32,对x连续应用两次f。用它计算apply_twice(|n| n + 3, 10)的结果。

  2. 三种Fn trait:解释以下代码为什么无法编译,并修复它:

    fn call_many_times<F: FnOnce()>(f: F) {
        f();
        f(); // 为什么这里报错?
    }
  3. 迭代器链:给定vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],用一条迭代器链找出所有偶数,计算它们的平方,然后求和。写出代码并说出结果。

  4. 自定义迭代器:实现一个Fibonacci结构体,让它实现Iterator trait,可以无限产出斐波那契数列。然后用.take(10).collect::<Vec<u64>>()获取前10个数。

  5. 综合应用:给定一组字符串vec!["hello world", "rust is great", "iterators rock"],用迭代器链完成:把每个字符串按空格拆分成单词,展平成一个单词列表,过滤掉长度小于4的单词,全部转为大写,然后收集成Vec<String>

💡 自我评估

  • 答对5题:闭包和迭代器已融会贯通,你可以用函数式风格写出优雅高效的Rust代码了。
  • 答对3-4题:核心概念已掌握,建议多练习迭代器链的组合使用和自定义迭代器的实现。
  • 答对0-2题:建议从简单的闭包和map/filter/collect三件套开始,逐步增加复杂度。理解惰性求值是掌握迭代器的关键转折点。

购买课程解锁全部内容

内存安全 + 零成本抽象:Rust 系统编程实战

¥29.90