闭包和迭代器
闭包
官方定义: 闭包(closures)是可以保存在变量中或作为参数传递给其他函数的匿名函数
||代表一个闭包,|x| x + 1代表一个接受一个参数并返回该参数加1的闭包- Rust的闭包本质是一个匿名函数,是一个语法糖,它背后是一个实现了
Fn、FnMut或FnOncetrait 的匿名类型 - 不同的是它能够捕获上下文的变量
闭包工作原理
闭包是个"隐形结构体"
当你写下这段代码时:
let x = 10;
let add_x = |y| x + y; // 这是一个闭包编译器在幕后其实为你偷偷写了一个结构体:
// 编译器生成的"隐形"结构体
struct Closure {
x: i32, // 它把外面的 x "抓"进来了
}
// 编译器为它"重载"了括号运算符(实现 Fn Trait)
impl Fn(i32) -> i32 for Closure {
fn call(&self, y: i32) -> i32 {
self.x + y
}
}推断和注解闭包类型
- 闭包通常不要求像
fn函数那样对参数和返回值进行类型注解,大部分情况下编译器会根据闭包的使用情况推断出类型
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;- 对于闭包定义,编译器会为每个参数和返回值推断出一个具体类型,不可以在同一闭包中使用不同类型的参数或返回值
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5); // 错误: 尝试给闭包使用不同的类型第一次使用
String值调用example_closure时,编译器推断出x的类型以及闭包的返回类型为String.接着这些类型被锁定进闭包example_closure中,如果尝试对同一闭包使用不同类型则会得到类型错误.
捕获引用或移动所有权
- 闭包可以捕获其环境中的变量,捕获方式有三种:不可变借用、可变借用和获取所有权
- 闭包将根据函数体中对捕获值的操作来自动决定使用哪种方式
let x = 5;
let equal_to_x = |z| z == x; // 闭包捕获了 x 的不可变引用
let mut y = 5;
let mut add_to_y = |z| y += z; // 闭包捕获了 y 的可变引用
let s = String::from("hello");
let consume_s = || drop(s); // 闭包获取了 s 的所有权
// 使用 move 关键字强制闭包获取所有权
let m = 5;
let consume_m = move |z| m + z;闭包的三种 Trait
- 闭包对环境变量的捕获方式决定了它实现的 Trait
- 函数在使用闭包时会根据所需的 Trait 来约束参数
| Trait | 捕获方式 | 适用场景 | 约束 |
|---|---|---|---|
Fn | 不可变借用 | 只需要读取环境变量的值 | 最严,只读 |
FnMut | 可变借用 | 需要修改环境变量的值 | 中等,可读可写 |
FnOnce | 获取所有权 | 需要获取环境变量的所有权 | 最松,可移动 |
Fn是FnMut的子 trait,FnMut是FnOnce的子 trait.实现
Fn的闭包也自动实现FnMut和FnOnce.这里的继承是"约束的继承", 而不是"能力的扩张",实现了
Fn的约束也就满足了FnMut和FnOnce的约束,所以实现了Fn就同时实现了FnMut和FnOnce
将捕获的值移出闭包
- 闭包默认情况下无法直接将捕获的所有权移出,但可以通过以下技巧实现:
let s = String::from("hello");
let consume_s = || drop(s); // 闭包获取了 s 的所有权并在内部消费
// 通过 Option<T> 和 take() 方法来移出值
let mut optional_s = Some(String::from("hello"));
let consume_optional_s = || optional_s.take(); // 返回 Option,可获取所有权迭代器
官方定义: 迭代器(iterator)负责遍历序列中的每一项并确定序列何时结束的逻辑
简单来说: 只要一个类型实现了 Iterator trait,它就是迭代器
- Rust 的迭代器是惰性的,只有最终调用
collect等消费方法时才会真正执行迭代器的逻辑 - 迭代器的核心是
next方法,每次调用都会返回序列中的下一项,直到序列结束时返回None - 迭代器适用于各种数据结构,不仅限于数组或向量,还可以是任何实现了
Iteratortrait 的类型 - 迭代器可以通过链式调用来组合多个操作,如过滤、映射等,从而实现复杂的数据处理逻辑
Iterator trait 和 next 方法
- 迭代器都实现了名为
Iterator的定义于标准库的 trait.这个 trait 的定义看起来像这样:
pub trait Iterator {
// 1. 关联类型: 告诉迭代器每次"吐出"的数据是什么类型
type Item; // 关联类型,一个类型只能为一个类型实现一次这个 trait
// 2. 核心方法: 定义如何获取下一个值
fn next(&mut self) -> Option<Self::Item>;
// 逻辑:
// 如果还有值,返回 Some(value)
// 如果迭代结束,返回 None
}
// 将自己的类型变成迭代器
impl Iterator for MyType {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
// 逻辑
}
}
Iteratortrait 里面定义了next方法,该方法每次返回迭代器中的一个项,封装在Some中,并且当迭代完成时,返回None只要你实现了
next方法,Rust 编译器就会"自动赠送"你几十个方法(迭代器适配器),比如.map(),.filter(),.sum(),.collect()等
- 调用
next方法
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter(); // 创建一个迭代器,调用 iter 方法会返回一个实现了 Iterator trait 的类型,它的 Item 类型是 &i32
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}注意我们需要将
v1_iter声明为可变的: 在迭代器上调用next方法会改变迭代器内部的状态,该状态用于跟踪迭代器在序列中的位置
为什么直接使用next方法需要可变引用,而消费迭代器的方法不需要
next方法需要可变引用是因为它需要修改迭代器的内部状态(比如当前索引),而消费迭代器的方法(如sum,collect)通常会获取迭代器的所有权,因此不需要可变引用- 在消费方法内部获取取迭代器的所有权后,将变量重新绑定为
mut, 从而内部的方法可以使用&mut调用next来遍历迭代器
let v1 = vec![1, 2, 3];
let total: i32 = v1.iter().sum(); // sum 方法获取迭代器的所有权,在内部重新绑定为 mut 来调用 next 方法
// sum 方法内部的逻辑大致如下:
fn sum<S>(self) -> S
where
Self: Sized,
S: std::iter::Sum<Self::Item>,
{
let mut iter = self; // 获取迭代器的所有权,重新绑定为 mut
let mut total = S::default(); // 初始化总和
while let Some(item) = iter.next() { // 使用 &mut iter 来调用 next 方法
total = total + item; // 累加每个项的值
}
total // 返回总和
}产生迭代器的方法
- 简单理解就是产生一个新的迭代器的方法,比如
iter方法会产生一个新的迭代器 - 这些方法产生的迭代器是惰性的,只有在调用消费方法时才会真正执行迭代器的逻辑
- 可以链式调用这些方法来组合多个操作,从而实现复杂的数据处理逻辑
- 本质是给这些数据集合实现
IntoIteratortrait,从而使它们能够被迭代器方法使用
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); // 需要使用 collect 方法来消费迭代器并将结果收集到一个新的向量中
assert_eq!(v2, vec![2, 3, 4]);迭代器的三种产生方式
| 方法 | 产生的类型 | 对原集合的影响 | 闭包里的参数 |
|---|---|---|---|
iter() | &T | 无影响(可继续读取) | &T |
iter_mut() | &mut T | 值可能被修改 | &mut T |
into_iter() | T | 原集合消失 | T (但 filter 会给 &T) |
还有其他能够在迭代器上产生新迭代器的方法,比如
mapfilterenumerate等,它们会返回一个新的迭代器,该迭代器会对原始迭代器中的每一项应用一个函数
iter()方法产生一个迭代器,迭代器中的项是集合中元素的不可变引用(&T).使用iter()不会改变原集合,你仍然可以继续使用它来读取数据.iter()做的事情非常轻量: 申请一个记录了起始位置的小纸条(结构体),并赋予它一套"往后挪一步"的方法(next实现)rustlet v = vec![1, 2, 3]; // 产生的是 &i32 (引用) let count = v.iter().count(); println!("v 依然可用: {:?}", v);iter_mut()方法产生一个迭代器,迭代器中的项是集合中元素的可变引用(&mut T).使用iter_mut()可能会修改原集合中的元素.rustlet mut v1 = vec![1, 2, 3]; // 原地修改,不需要 collect,也不产生新 Vec v1.iter_mut().for_each(|x| *x *= 2); println!("v1 被原地改了: {:?}", v1); // [2, 4, 6]into_iter()方法产生一个迭代器,迭代器中的项是集合中元素的所有权(T).使用into_iter()会消耗原集合,原集合将不再可用.rustlet v2 = vec![1, 2, 3]; // 产生的是 i32 (所有权) let v2_iter = v2.into_iter().collect::<Vec<i32>>(); println!("v2 不能再使用了: {:?}", v2); // error: borrow of moved value: `v2`
能转成迭代器的类型
- 迭代器不仅限于数组或向量,还可以是任何实现了
Iteratortrait 的类型 - 标准库有些类型(如Range)直接实现了
Iteratortrait,而大多数集合类型(Vec、HashMap等)只提供了转成成迭代器的方法(iter、iter_mut、into_iter),它们本身并不是迭代器 - 你也可以为自己的类型实现
Iteratortrait,从而使它们能够被迭代器方法使用
1. 标准库中的所有集合(Collections) 几乎所有存数据的容器都能产生迭代器:
- 线性集合:
Vec<T>、VecDeque<T>、LinkedList<T>. - 键值对/集合:
HashMap<K, V>、BTreeMap<K, V>、HashSet<T>、BTreeSet<T>. - 原生数组:
[T; N](如[1, 2, 3])以及切片&[T].
2. 字符串相关 字符串稍微特殊一点,因为它必须明确你是按"字节"还是按"字符"迭代:
String或&str:.chars(): 产生字符迭代器(处理 Unicode)..bytes(): 产生原始字节迭代器..lines(): 按行迭代..split_whitespace(): 按空格迭代.
3. 其他常见类型
Option<T>和Result<T, E>: 它们也实现了迭代器协议!你可以把它们看作是一个"长度最多为 1 的集合".- 例子:
Some(5).iter()会产生一个只吐出一次 5 的迭代器.
- 例子:
Range(范围): 如(0..10)或(0..=10).它们本身就是为了迭代而生的.
4. 外部资源(流式数据)
- 文件/网络: 如
std::io::BufReader的.lines(),它允许你逐行读取大文件而不需要一次性加载到内存.
核心判断标准:
如果你不确定一个数据能不能调 .iter(),你可以看它的文档:
- 它是否实现了
IntoIterator?(如果有,它就能用在 for 循环里). - 它是否实现了
Deref到切片?(比如Vec<T>可以自动变成&[T],而切片是有.iter()的).
一句话总结:
凡是"装着一堆东西"(集合)或者"能产生一串东西"(范围、流)的类型,基本都能转换成迭代器.
常用迭代器适配器
- 就是那些返回新迭代器的方法,调用它们不会立即执行迭代器的逻辑,而是返回一个新的迭代器,这些方法被称为迭代器适配器,因为它们适配了原始迭代器并返回一个新的迭代器
- 惰性,返回新迭代器
| 方法 | 说明 |
|---|---|
map(f) | 对每个元素做变换,返回新迭代器 |
filter(p) | 保留满足条件的元素 |
enumerate() | 带索引遍历,得到 (index, item) |
zip(other) | 将两个迭代器压缩成元组迭代器 (a, b) |
chain(other) | 连接两个迭代器,顺序遍历 |
flat_map(f) | map 后展平一层(等价于 .map(f).flatten()) |
flatten() | 展平嵌套迭代器(Vec<Vec<T>> → Vec<T>) |
take(n) | 只取前 n 个元素 |
skip(n) | 跳过前 n 个元素 |
step_by(n) | 每隔 n 步取一个元素 |
peekable() | 让迭代器可以 .peek() 而不消耗元素 |
cloned() | 将 &T 迭代器转为 T 迭代器(T 需实现 Clone) |
copied() | 将 &T 迭代器转为 T 迭代器(T 需实现 Copy) |
let v = vec![1, 2, 3, 4, 5, 6];
// enumerate + filter + map 链式组合
let result: Vec<String> = v.iter()
.enumerate()
.filter(|(_, x)| *x % 2 == 0)
.map(|(i, x)| format!("索引{i}: {x}"))
.collect();
// zip: 两个迭代器合并成元组
let names = vec!["Alice", "Bob"];
let scores = vec![95, 87];
let combined: Vec<_> = names.iter().zip(scores.iter()).collect();
// [("Alice", 95), ("Bob", 87)]
// flat_map: 展开嵌套
let words = vec!["hello world", "foo bar"];
let chars: Vec<&str> = words.iter().flat_map(|s| s.split(' ')).collect();
// ["hello", "world", "foo", "bar"]消费迭代器的方法
- 简单理解就是不返回迭代器本身而是返回一个具体的值的方法,比如
sum方法会消耗迭代器并返回迭代器中所有项的总和 Iteratortrait 有一系列不同的由标准库提供默认实现的方法;你可以在Iteratortrait 的标准库 API 文档中找到所有这些方法- 这些方法在其内部调用
next方法来处理迭代器中的项,next方法本身并不会消耗迭代器,是这些方法是按值传递迭代器,所以它们会消耗迭代器(获取所有权)并返回一个具体的值
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum(); // sum 方法会消耗 v1_iter,并返回迭代器中所有项的总和
assert_eq!(total, 6);
}常用消费型方法
- 消耗迭代器,返回具体值
| 方法 | 说明 |
|---|---|
collect::<Vec<_>>() | 收集成集合(Vec、HashMap 等) |
count() | 元素总数 |
sum() / product() | 求和 / 求积 |
any(p) / all(p) | 任意 / 全部满足条件 → bool |
find(p) | 首个满足条件的元素 → Option<&T> |
position(p) | 首个满足条件的索引 → Option<usize> |
max() / min() | 最大 / 最小值 → Option<T> |
fold(init, f) | 折叠累积(类似 JS 的 reduce) |
for_each(f) | 遍历并执行副作用(等价于 for 循环) |
let v = vec![1, 2, 3, 4, 5];
let sum: i32 = v.iter().sum(); // 15
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let has_even = v.iter().any(|x| x % 2 == 0); // true
let all_pos = v.iter().all(|x| *x > 0); // true
let first_gt3 = v.iter().find(|&&x| x > 3); // Some(4)
let total = v.iter().fold(0, |acc, x| acc + x); // 15(等同于 sum)使用捕获其环境的闭包
- 迭代器的方法通常接受一个闭包作为参数,该闭包定义了如何处理迭代器中的每一项
- 这些闭包可以捕获其环境中的变量,从而允许你在迭代器的操作中使用外部数据
- 例如,
filter方法接受一个闭包,该闭包返回一个布尔值来决定是否保留迭代器中的某项
let v1 = vec![1, 2, 3, 4, 5];
let even_numbers: Vec<_> = v1.iter().filter(|&x| x % 2 == 0).collect();
assert_eq!(even_numbers, vec![2, 4]);额外补充
for 循环背后的迭代器
当你直接对集合使用 for 循环时,Rust 会自动调用不同的方法:
for x in &vec自动调用vec.iter()for x in &mut vec自动调用vec.iter_mut()for x in vec自动调用vec.into_iter()(注意: 这会毁掉 vec)