找回密码
立即注册
搜索
发回帖 发新帖

6699

积分

0

好友

841

主题
发表于 10 小时前 | 查看: 5| 回复: 0

本篇导读

今天是第一卷的收官篇。我们学习 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 的组合方式:声明式、清晰、零开销。


八、本篇小结

  1. 闭包是 |参数| 表达式 形式的匿名函数,能捕获定义环境;类型可由首次调用推断;
  2. 捕获三 trait:只读借用为 Fn、可变借用为 FnMut、移动消耗为 FnOnce,按需作为参数约束;move 强制获取所有权;
  3. 迭代器实现 Iterator trait,核心是 next 返回 Option<Item>;iter() 出引用、iter_mut() 出可变引用、into_iter() 出所有权;
  4. 适配器(map / filter / take / skip / zip / enumerate)惰性,消费者(collect / sum / product / count / max / min / any / all / fold / for_each)触发执行;
  5. 迭代器链经内联编译,是零成本抽象;
  6. 为自定义类型实现 next 即可获得全部迭代器能力。

九、第一卷结语

恭喜你!到这里,第一卷“筑基”全部完成。你已经掌握了:变量与类型、所有权/借用/切片、结构体/枚举、模块系统、集合、错误处理、泛型/trait/生命周期、测试,以及闭包与迭代器,并完成了 minigrep 实战。

你已经具备了独立阅读和编写中等规模 Rust 程序的能力。 从下一篇开始进入第二卷“登堂”,我们将探索智能指针、无畏并发、宏与异步——那些让 Rust 真正强大、并区别于一切其他语言的进阶主题。


十、动手练习

  1. 写一个闭包捕获一个 Vec<i32> 并对其求和(分别尝试 Fn 与 move 版本,观察所有权差异);
  2. 用迭代器重写第 3 篇的素数打印:(2..n).filter(|x| ...).collect();
  3. 用 fold 实现一个函数,求切片的最大值(不使用内置 max);
  4. 实现一个从 start 到 end 的范围迭代器结构体;
  5. (选做)用闭包 + 迭代器实现一个简单的“单词频率”函数,并按频率从高到低排序输出。

下篇预告:第 16 篇|智能指针(上):Box 与 Deref

第二卷开始!我们将深入 Rust 的指针世界:什么是智能指针、Box<T> 如何在堆上分配值、Deref trait 如何让自定义指针像普通引用一样使用、Drop trait 如何控制资源清理。

我们还会用 Box 实现递归类型(如链表)。下篇见。




上一篇:4.4k 星 AI 越狱提示词仓库拆解:七层手法从身份层打到部署层
下一篇:Rust智能指针详解:Box、Deref与Drop,从堆分配到资源清理
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-28 14:16 , Processed in 0.089690 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表