Rust中的闭包
1.基础
基本语法
Rust 中的闭包使用 |参数| 表达式
的语法:
rust
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 输出 6
闭包与函数的区别
- 类型推断:闭包不需要显式声明参数和返回值的类型(可以省略)
- 环境捕获:闭包可以捕获其定义作用域中的变量
- 语法简洁:闭包通常比函数更简洁
2.闭包的类型
Rust 闭包实现了以下 trait 之一(按灵活性递减顺序):
Fn
:不可变借用捕获环境FnMut
:可变借用捕获环境FnOnce
:获取所有权捕获环境,只能调用一次
示例:不同类型的闭包
rust
// Fn 闭包
let print_message = |msg: &str| println!("{}", msg);
print_message("Hello, Fn!");
// FnMut 闭包
let mut count = 0;
let mut increment = || {
count += 1;
println!("Count: {}", count);
};
increment(); // Count: 1
increment(); // Count: 2
//FnOnce 闭包
let s = String::from("Hello");
let consume_string = move || {
let a = s; //消耗掉s
println!("{}", a);
// s 在这里被移动
};
consume_string(); // 第一次调用正常
//consume_string(); // 错误!闭包已经消耗,不能再次调用
不同类型闭包的详细对比
核心区别在于 如何访问捕获的变量 以及 能否被多次调用。
1.Fn
闭包(不可变借用)
-
特点:
- 通过 不可变引用 (
&T
)捕获环境变量。 - 可以 多次调用,不会消耗闭包自身或捕获的变量。
- 通过 不可变引用 (
-
使用场景:
- 只需读取环境变量,无需修改。
- 需要重复调用的闭包(如迭代器操作、回调函数)。
-
示例:
rustlet x = 10; let print_x = || println!("x = {}", x); // 不可变借用 x print_x(); // 可多次调用 print_x();
2.FnMut
闭包(可变借用)
-
特点:
- 通过 可变引用 (
&mut T
)捕获环境变量。 - 可以 多次调用,但每次调用可能修改捕获的变量。
- 闭包自身需要声明为
mut
。
- 通过 可变引用 (
-
使用场景:
- 需要修改外部变量的闭包。
- 状态保持或累计操作(如计数器)。
-
示例:
rustlet mut count = 0; let mut increment = || { count += 1; // 可变借用 count println!("Count: {}", count); }; increment(); // Count: 1 increment(); // Count: 2
3.FnOnce
闭包(所有权转移)
-
特点:
- 通过 所有权 (
T
)捕获环境变量。 - 只能调用一次,调用后闭包和捕获的变量会被消耗。
- 通常用于需要转移所有权的场景(如跨线程传递数据)。
- 通过 所有权 (
-
使用场景:
- 捕获的资源需要在闭包内被消耗(如
String
、Vec
)。 - 需要移动所有权的操作(如
thread::spawn
)。
- 捕获的资源需要在闭包内被消耗(如
-
示例:
rust//FnOnce 闭包 let data = vec![1, 2, 3]; let consume_data = move || { let drop_data = data; //消耗掉data println!("Data: {:?}", drop_data); // data 在这里被移动,无法再次使用 }; consume_data(); //consume_data(); // 错误!闭包只能调用一次
关键区别总结
特性 | Fn |
FnMut |
FnOnce |
---|---|---|---|
捕获方式 | 不可变引用(&T ) |
可变引用(&mut T ) |
所有权(T ) |
调用次数 | 多次 | 多次 | 仅一次 |
是否需要 mut |
否 | 是(闭包变量需 mut ) |
否 |
典型用途 | 无副作用操作 | 修改外部状态 | 消耗资源或跨线程 |
自动推导规则
Rust 编译器会根据闭包内部行为自动推断类型:
- 如果闭包 只读取 环境变量 →
Fn
。 - 如果闭包 修改 环境变量 →
FnMut
。 - 如果闭包 移动 环境变量(如
move
关键字且所有权发生转移)→FnOnce
。
强制指定类型
在函数签名中,可以显式指定闭包类型:
rust
fn call_fn<F: Fn()>(f: F) { f() }
fn call_fn_mut<F: FnMut()>(mut f: F) { f() }
fn call_fn_once<F: FnOnce()>(f: F) { f() }
为什么 FnOnce
是超级 Trait?
Fn
和FnMut
都是FnOnce
的子 Trait:- 所有
Fn
闭包也是FnOnce
(因为不可变借用不阻止所有权转移)。 - 所有
FnMut
闭包也是FnOnce
。
- 所有
- 因此,接受
FnOnce
的函数可以兼容所有闭包,但会限制调用次数。
实际应用建议
- 优先用
Fn
:除非需要修改或移动变量。 - 线程安全 :跨线程时用
move + FnOnce
确保所有权转移。 - 性能 :
Fn
和FnMut
通常零开销,FnOnce
可能涉及内存分配。
3.闭包作为函数参数
闭包作为函数参数是Rust函数式编程的重要特性,它提供了极大的灵活性。我们可以通过多种方式将闭包传递给函数,每种方式都有其适用场景和特点。
1.基本用法(静态)
最直接的方式是使用泛型和trait约束来接收闭包:
rust
fn main() {
let double = |x| x * 2;
println!("{}", apply(double, 5)); // 输出 10
let square = |x| x * x;
println!("{}", apply(square, 5)); // 输出 25
}
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
特点:
- 使用泛型参数
F
和Fn
trait约束 - 编译时静态分发,性能最优
- 适用于大多数不需要修改捕获变量的场景
2.使用 trait 对象(动态)
当需要运行时动态分发时,可以使用trait对象:
rust
fn main() {
let mut counter = 0;
let mut increment = || {
counter += 1;
println!("Counter: {}", counter);
};
call_twice(&mut increment);
// 输出:
// Counter: 1
// Counter: 2
}
fn call_twice(f: &mut dyn FnMut()){
f();
f();
}
特点:
- 使用
dyn FnMut
trait对象 - 运行时动态分发,有一定性能开销
- 允许修改捕获的变量(使用
FnMut
) - 适用于需要动态选择闭包的场景
3.静态分发详解
实现方式
- 通过 泛型(Generics) 和 trait 约束 实现。
- 编译器会为每个具体类型生成独立的代码(单态化,Monomorphization)。
- 在编译期确定调用的具体方法。
代码示例
rust
fn main() {
let double = |x| x * 2;
println!("{}", apply(double, 5));//输出:10 // 编译时生成 `apply::<double>` 专用代码
}
fn apply<F> (f:F,x:i32) -> i32
where
F: Fn(i32) -> i32, // 静态分发
{
f(x)
}
底层原理
编译器会为每个不同的 F
类型生成一个独立的 apply
版本:
rust
// 编译器生成的代码示例
fn apply_for_double(f: impl Fn(i32)->i32, x: i32) -> i32 {
f(x) // 直接调用,无额外开销
}
使用场景
- 需要最高性能(如数值计算、算法)
- 类型在编译期已知(大多数泛型代码)
- 避免运行时开销(如嵌入式系统)
4.动态分发详解
实现方式
- 通过 trait 对象(
dyn Trait
) 实现。 - 使用 虚表(vtable) 在运行时查找正确的方法。
- 在运行期决定调用的具体方法。
代码示例
rust
fn main() {
let mut counter = 0;
let mut increment = || {
counter += 1;
println!("Counter: {}", counter);
};
call_twice(&mut increment); // 传递 trait 对象
//输出:
//Counter: 1
//Counter: 2
}
fn call_twice(f: &mut dyn FnMut()){ // 动态分发
f(); // 通过虚表查找调用
f();
}
底层原理
编译器会生成一个 虚表(vtable),存储函数指针:
rust
// 近似底层表示
struct VTable {
call_fn: fn(*mut ()), // 函数指针
}
let vtable = VTable { call_fn: increment_implementation };
let trait_obj = &mut increment as &mut dyn FnMut(); // 包含数据指针和虚表指针
调用时:
rust
// 动态调用过程
(trait_obj.vtable.call_fn)(trait_obj.data);
示例解释
1.increment
是一个闭包,也是 trait 对象
rust
let mut increment = || {
counter += 1;
println!("Counter: {}", counter);
};
increment
是一个闭包,它捕获了外部变量counter
的可变引用- 这个闭包自动实现了
FnMut
trait(因为它需要修改捕获的变量) - 当传递
&mut increment
给call_twice
时,它被强制转换为 trait 对象&mut dyn FnMut()
2.FnMut()
是一个 trait
FnMut()
确实是 Rust 标准库中定义的 trait,它的简化定义如下:
rust
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
FnMut
是 Rust 的三个闭包 trait(FnOnce
、FnMut
、Fn
)之一- 它表示可以多次调用且可以修改其环境的闭包
- 当你写
dyn FnMut()
时,就是在使用这个 trait 的动态分发版本
3.类型转换过程
rust
call_twice(&mut increment);
这里发生了隐式的类型转换:
increment
是一个具体的闭包类型(编译器生成的匿名结构体)&mut increment
是这个闭包类型的可变引用- 当传递给期望
&mut dyn FnMut()
的函数时,Rust 会自动进行"胖指针"转换:- 数据指针:指向闭包数据
- 虚表指针:指向该闭包类型的
FnMut
实现
4.动态分发机制
在 call_twice
函数内部:
rust
fn call_twice(f: &mut dyn FnMut()) {
f(); // 通过虚表查找调用
f();
}
每次调用 f()
时:
1.通过虚表查找 call_mut
方法
2.传递闭包数据作为 &mut self
参数
3.执行实际的闭包代码
5.闭包与 trait 对象的关系
- 所有闭包都会自动实现适当的闭包 trait(
Fn
、FnMut
或FnOnce
) - 当使用
dyn
前缀时,闭包可以被当作 trait 对象使用 - 这种转换使得我们可以统一处理不同类型的闭包
6.对比静态分发
如果使用泛型而非 trait 对象,代码会是:
rust
fn call_twice<F: FnMut()>(f: &mut F) {
f();
f();
}
这种静态分发版本:
- 会为每种闭包类型生成专用代码
- 性能更好(无运行时开销)
- 但会导致代码膨胀(多个实例)
而动态分发版本:
- 只有一份代码
- 有运行时虚表查找开销
- 但可以处理任意实现了
FnMut
的类型
使用场景
- 需要运行时多态(如 GUI 事件处理器)
- 处理异构集合(如
Vec<Box<dyn Draw>>
) - 实现插件系统或动态加载
概念解析
trait对象
1.基本概念
Trait 对象是通过将指针 (引用或智能指针)与 trait 结合创建的,形式为 &dyn Trait
、Box<dyn Trait>
等。其中 dyn
关键字明确表示这是动态分发。
2.创建Trait 对象
rust
fn main() {
let dog = Dog;
let cat = Cat;
//创建 trait 对象
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
}
trait Animal{
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
struct Cat;
impl Animal for Cat{
fn speak(&self) {
println!("Meow!");
}
}
3.Trait 对象的组成
每个 trait 对象实际上由两个指针组成:
-
数据指针:指向实际实例的数据
-
虚表指针(vtable):指向该类型的 trait 实现方法列表
rust
struct TraitObject {
data: *mut (),
vtable: *mut VTable,
}
4.使用场景
- 异构集合:存储多种不同类型但实现相同 trait 的对象
rust
fn make_sounds(animals: Vec<&dyn Animal>) {
for animal in animals {
animal.speak();
}
}
- 返回多种可能类型:
rust
fn get_pet(kind: &str) -> Box<dyn Animal> {
match kind {
"dog" => Box::new(Dog),
"cat" => Box::new(Cat),
_ => panic!("Unknown pet")
}
}
- 回调函数和事件处理:
rust
trait EventHandler {
fn handle(&self, event: Event);
}
struct App {
handlers: Vec<Box<dyn EventHandler>>
}
5.对象安全
不是所有 trait 都能作为 trait 对象使用。只有满足"对象安全"的 trait 才可以:
-
不能返回
Self
-
不能有泛型方法
-
不能使用
Self
作为参数类型(除了接收者self
)
为什么需要对象安全规则?
Trait 对象在运行时通过虚表(vtable)查找方法,这意味着编译器必须能够为 trait 的所有方法生成确定性的函数指针。如果 trait 违反了对象安全规则,编译器就无法生成有效的虚表。
对象安全的详细规则
-
不能返回
Self
-
原因:Trait 对象会擦除具体类型信息,无法在运行时确定
Self
的具体类型 -
错误例子:
rusttrait Clone { fn clone(&self) -> Self; // 编译错误:不能返回 Self }
-
-
不能有泛型方法
-
原因:泛型方法需要为每个具体类型生成单独的代码,与 trait 对象的单份代码理念冲突
-
错误例子:
rusttrait Serializer { fn serialize<T>(&self, value: T); // 编译错误:不能有泛型方法 }
-
-
不能使用
Self
作为参数类型(除了接收者self
)-
原因:同样因为类型擦除,无法在运行时确定
Self
的具体类型 -
错误例子:
rusttrait Equals { fn equals(&self, other: Self); // 编译错误:参数不能是 Self }
-
6.生命周期
Trait 对象可以指定生命周期:
rust
trait Process {
fn process(&self);
}
fn process_all<'a>(items: Vec<&'a dyn Process>) {
for item in items {
item.process();
}
}
5.关键区别对比
特性 | 静态分发 | 动态分发 |
---|---|---|
决定时机 | 编译期 | 运行期 |
性能 | 零开销,直接调用 | 有间接调用开销 |
代码生成 | 单态化(可能代码膨胀) | 共享代码(无膨胀) |
灵活性 | 编译时类型固定 | 运行时可替换实现 |
内存占用 | 可能较大(多份代码) | 较小(虚表+指针) |
典型用例 | 泛型函数、迭代器 | trait 对象、插件系统 |
6.高级技巧
静态分发的优化
rust
// 使用 `impl Trait` 简化签名
fn apply(f: impl Fn(i32)->i32, x: i32) -> i32 {
f(x)
}
动态分发的灵活用法
rust
// 返回 trait 对象(动态分发)
fn make_closure() -> Box<dyn Fn(i32)->i32> {
Box::new(|x| x * 2)
}
混合使用
rust
// 静态分发为主,动态分发备用
fn process<T: AsRef<str>>(s: T) {
println!("{}", s.as_ref()); // 静态分发优先
}
fn process_dyn(s: &dyn AsRef<str>) {
println!("{}", s.as_ref()); // 动态分发备用
}
总结
- 静态分发 = 编译器生成专用代码 + 直接调用 → 高性能
- 动态分发 = 运行时虚表查找 → 灵活性
- Rust 允许混合使用两者,根据场景选择最优方案。
4.闭包高级用法
闭包是 Rust 中非常强大的特性,除了基础用法外,还有许多高级技巧可以写出更灵活、更高效的代码。下面介绍一些 Rust 闭包的高级用法和技巧。
1.闭包类型系统进阶
三种闭包 trait
Rust 闭包自动实现以下 trait 之一或多个:
FnOnce
: 能调用一次,会消耗捕获的变量FnMut
: 能多次调用,可修改捕获的变量Fn
: 能多次调用,不可修改捕获的变量(共享引用)
rust
fn main() {
let double = |x| x * 2;
assert_eq!(call_with_one(double), 2);
}
fn call_with_one<F>(func: F) -> i32
where
F: Fn(i32) -> i32,
{
func(1)
}
2.闭包捕获模式控制
强制移动捕获
使用 move
关键字强制闭包取得变量的所有权:
rust
fn main() {
let nums = vec![1, 2, 3];
let owns_nums = move || {
println!("Captured nums: {:?}", nums);
// nums 的所有权已转移到闭包内
};
owns_nums();
// println!("{:?}", nums); // 错误!nums 已被移动
}
选择性捕获
通过重构代码控制闭包捕获的内容:
rust
fn main() {
let mut count = 0;
let mut inc = || {
count += 1; // 只使用 count
println!("Count:{}", count);
};
inc();
inc();
}
3.闭包作为返回值
1.使用 impl Trait
返回闭包(更高效)
rust
fn main() {
let triple = make_multiplier(3);
assert_eq!(triple(5), 15);
}
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
2.使用 Box<dyn Fn>
返回闭包
需要使用 Box
进行堆分配:
rust
fn main() {
let add5 = make_adder(5);
assert_eq!(add5(3), 8);
}
fn make_adder(x:i32) -> Box<dyn Fn(i32) -> i32>{
Box::new(move |y| x + y)
}
3.两种方式对比
返回闭包的两种方式(Box<dyn Fn>
和 impl Fn
)有重要的区别,主要体现在 性能、灵活性 和 使用场景 上。以下是它们的对比:
1.静态分发
rust
fn make_multiplier(x: i32) -> impl Fn(i32) -> i32 {
move |y| x * y
}
特点:
- 栈分配(Stack Allocation):闭包直接存储在栈上,无堆分配开销。
- 静态分发(Static Dispatch):编译器会单态化(monomorphize)闭包,调用时无运行时开销。
- 零成本抽象:性能最优,和直接写闭包一样高效。
- 只能返回一种闭包类型:编译器必须知道具体的返回类型(不能有多个分支返回不同的闭包)。
适用场景:
- 性能敏感的场景(如高频调用的闭包)。
- 闭包类型单一,不需要动态返回多种闭包。
2.动态分发
rust
fn make_adder(x: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |y| x + y)
}
特点:
- 堆分配(Heap Allocation):闭包被 Box 分配到堆上,带来额外的内存开销。
- 动态分发(Dynamic Dispatch):由于返回的是 dyn Fn,调用闭包时会有额外的虚函数表(vtable)查找开销。
- 灵活性:可以返回不同类型的闭包(只要它们都实现了 Fn(i32) -> i32)。
- 必须显式 Box:需要手动封装闭包。
适用场景:
- 需要返回多种可能的闭包类型(例如,条件分支返回不同的闭包)。
- 闭包的生命周期较长,需要存储在堆上(例如,作为长期存在的回调函数)。
其他:
Box<dyn Fn(i32) -> i32> 这个 返回值 一定要放在Box里面吗?
在 Rust 中,如果直接返回闭包(比如 move |y| x + y
),必须使用 Box
进行堆分配 (即 Box<dyn Fn>
),原因如下:
1.闭包的大小在编译时未知
Rust 要求函数的返回类型必须是固定大小的,但闭包是匿名类型,每个闭包的实际类型是编译器生成的唯一结构(无法提前知道大小)。例如:
rust
let closure1 = |y| 5 + y; // 编译器生成一个匿名类型 `Closure1`
let closure2 = |y| 10 + y; // 编译器生成另一个匿名类型 `Closure2`
这两个闭包的类型不同,无法直接作为函数返回值。
2.Box的作用
Box
是一个堆分配的指针,它的大小是固定的(通常是一个 usize,即 8 字节),因此可以满足返回类型Sized的要求:
rust
Box::new(move |y| x + y) // 将闭包移到堆上,返回固定大小的指针
3.替代方案:impl Fn(无需 Box)
如果你能确保函数只返回一种闭包类型(而不是动态多态),可以用impl Fn避免堆分配:
rust
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y // 无需 Box,闭包直接存储在栈上
}
这种方式更高效,但不能用于返回多种闭包类型(比如条件分支返回不同的闭包)。
3.关键区别对比
特性 | Box<dyn Fn> |
impl Fn |
---|---|---|
内存分配 | 堆分配(Box) | 栈分配(无额外开销) |
调用开销 | 动态分发(vtable 查找) | 静态分发(直接调用) |
灵活性 | 可返回多种闭包类型 | 只能返回一种闭包类型 |
适用场景 | 需要动态返回闭包 | 高性能、单一闭包类型 |
如何选择?
-
优先
impl Fn
:- 如果闭包类型单一,且性能重要(推荐默认使用)。
- 例如:工厂函数返回固定逻辑的闭包。
-
使用
Box<dyn Fn>
:- 需要返回 多种不同的闭包(如条件分支返回不同闭包)。
- 闭包需要长期存储(如作为回调存储在数据结构中)。
总结
impl Fn
更快,更符合零成本抽象,适用于固定闭包类型。Box<dyn Fn>
更灵活,适用于动态返回闭包或长期存储。- 在 Rust 2021+ 版本中,尽量优先使用
impl Fn
,除非必须动态分发。
4.高阶函数技巧
函数组合
rust
fn main() {
let add_one = |x| x + 1;
let square = |x| x * x;
let add_then_square = compose(add_one, square);
assert_eq!(add_then_square(2), 9);
}
fn compose<F,G,A,B,C>(f:F,g:G) -> impl Fn(A) -> C
where
F:Fn(A) -> B,
G:Fn(B) -> C,
{
move |x| g(f(x))
}
5.闭包与迭代器高级用法
自定义迭代器适配器
rust
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let evens: Vec<_> = nums.into_iter().my_filter(|&x| x % 2 == 0).collect();
assert_eq!(evens, vec![2, 4]);
}
trait MyIterExt: Iterator{
fn my_filter<F>(self,f:F) -> MyFilter<Self,F>
where
F: FnMut(&Self::Item) -> bool,
Self:Sized,
{
MyFilter { iter: self, f }
}
}
struct MyFilter<I,F>{
iter:I,
f:F,
}
impl <I:Iterator,F> Iterator for MyFilter<I,F>
where
F: FnMut(&I::Item) -> bool,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
while let Some (item) = self.iter.next() {
if (self.f)(&item) {
return Some(item);
}
}
None
}
}
impl <T: ?Sized> MyIterExt for T where T: Iterator{}
示例解释
1.main函数
rust
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let evens: Vec<_> = nums.into_iter().my_filter(|&x| x % 2 == 0).collect();
assert_eq!(evens, vec![2, 4]);
}
-
创建一个
Vec<i32>
,包含[1, 2, 3, 4, 5]
。 -
调用
into_iter()
将其转换为迭代器。 -
使用自定义的
my_filter
方法过滤出偶数(x % 2 == 0
)。 -
最终
collect()
收集结果,并断言evens == [2, 4]
。
2.MyIterExt Trait(扩展方法)
rust
trait MyIterExt: Iterator {
fn my_filter<F>(self, f: F) -> MyFilter<Self, F>
where
F: FnMut(&Self::Item) -> bool,
Self: Sized,
{
MyFilter { iter: self, f }
}
}
MyIterExt
是一个 trait ,为所有实现了Iterator
的类型(如Vec<T>
的迭代器)添加my_filter
方法。my_filter
:- 接收一个闭包
f: F
,用于过滤元素(FnMut(&Item) -> bool
)。 - 返回
MyFilter
结构体(一个自定义迭代器)。
- 接收一个闭包
Self: Sized
:- 限制
Self
必须是编译期已知大小的类型(因为my_filter
要按值返回MyFilter
)。
- 限制
3.MyFilter 结构体(自定义迭代器)
rust
struct MyFilter<I, F> {
iter: I, // 底层迭代器
f: F, // 过滤条件(闭包)
}
- 这是一个泛型结构体:
I
是底层迭代器(如std::vec::IntoIter<i32>
)。F
是闭包类型(如|&x| x % 2 == 0
)。
4.为 MyFilter 实现 Iterator
rust
impl<I: Iterator, F> Iterator for MyFilter<I, F>
where
F: FnMut(&I::Item) -> bool, // 闭包接受 `&Item` 并返回 `bool`
{
type Item = I::Item; // 输出类型和底层迭代器一致
fn next(&mut self) -> Option<Self::Item> {
while let Some(item) = self.iter.next() {
if (self.f)(&item) { // 调用闭包检查是否保留该元素
return Some(item); // 如果条件满足,返回该元素
}
}
None // 迭代结束
}
}
next
方法 :- 不断从底层迭代器
self.iter
获取下一个元素。 - 用闭包
self.f
检查该元素是否满足条件。 - 如果满足,返回
Some(item)
;否则继续循环。 - 如果底层迭代器耗尽,返回
None
。
- 不断从底层迭代器
5.为所有 Iterator 实现 MyIterExt
rust
impl<T: ?Sized> MyIterExt for T where T: Iterator {}
?Sized
表示T
可以是动态大小类型(DST),但这里Iterator
本身要求Self: Sized
,所以实际上T
必须是Sized
。- 这个实现让 所有
Iterator
类型 自动获得my_filter
方法。
6.与标准库 filter 的区别
特性 | 标准库 filter |
这里的 my_filter |
---|---|---|
闭包参数 | FnMut(&Self::Item) -> bool |
相同 |
返回类型 | Filter<Self, F> |
MyFilter<Self, F> |
实现方式 | 标准库内置 | 自定义 |
适用场景 | 通用 | 学习/自定义扩展 |
6.闭包与生命周期
闭包中的生命周期注解
rust
fn main() {
let processor = {
let text = String::from("hello"); // `text` 在这个块中创建
make_processor(&text) // 返回的闭包捕获了 `&text`
}; // `text` 在这里被销毁!
// 但 `processor` 还试图持有 `text` 的引用 → 悬垂指针!
}
fn make_processor<'a>(s: &'a str) -> Box<dyn Fn() -> &'a str + 'a> {
Box::new(move || s) // 闭包捕获 `s`(即 `&text`)
}
高阶生命周期模式
rust
fn main() {
// 定义一个包含1, 2, 3的Vec<i32>
let nums = vec![1, 2, 3];
// 调用apply_to_all函数,传入nums的引用和一个计算平方的闭包
// |x| x * x 是一个闭包,接收一个i32引用,返回它的平方值
let squared = apply_to_all(&nums, |x| x * x);
// 断言结果是否等于预期的vec![1, 4, 9]
assert_eq!(squared, vec![1, 4, 9]);
}
/// 对切片中的每个元素应用给定的闭包,返回处理后的结果集合
///
/// # 参数
/// * `items` - 一个 `i32` 类型的切片引用,生命周期标记为 `'a`
/// * `f` - 一个闭包,接受 `&'a i32` 并返回 `i32`
///
/// # 返回值
/// 返回一个新的 `Vec<i32>`,包含对每个元素应用闭包后的结果
///
/// # 泛型约束
/// * `F` - 必须实现 `FnMut` trait,表示可以可变地捕获环境
/// * 闭包参数和返回值的生命周期与输入切片一致 (`'a`)
fn apply_to_all<'a, F>(items: &'a [i32], f: F) -> Vec<i32>
where
F: FnMut(&'a i32) -> i32, // F是一个闭包,接受&i32返回i32
{
// 使用迭代器处理:
// 1. items.iter() - 创建切片的迭代器
// 2. .map(f) - 对每个元素应用闭包f
// 3. .collect() - 将结果收集到Vec中
items.iter().map(f).collect()
}
7.闭包与多线程
跨线程闭包
rust
use std::thread; // 导入标准库的线程模块
fn main() {
let data = vec![1, 2, 3]; // 创建一个 Vec<i32>
// 创建一个新线程
thread::spawn(move || { // `move` 关键字强制闭包获取 `data` 的所有权
println!("Data in thread: {:?}", data); // 在新线程中打印 data
})
.join() // 等待线程结束
.unwrap(); // 处理可能的错误
}
线程池中的闭包
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for i in 0..5 {
let tx = tx.clone();
thread::spawn(move || {
tx.send(i * 2).unwrap();
});
}
drop(tx); // 关键!关闭主线程的发送端
let results: Vec<_> = rx.iter().collect();
println!("{:?}", results); // 正常输出 [0, 2, 4, 6, 8] 顺序会有变动
}
8.闭包与模式匹配
闭包中的解构
rust
fn main() {
let pairs = vec![(1, "one"), (2, "two"), (3, "three")];
let strings: Vec<_> = pairs.into_iter().map(|(num,word)| format!("{}: {}", num, word)).collect();
assert_eq!(strings, vec!["1: one", "2: two", "3: three"]);
}
复杂模式匹配
rust
fn main() {
let points = vec![Point { x: 1, y: 2 }, Point { x: 3, y: 4 }];
let x_coords: Vec<_> = points.into_iter().map(|Point { x, .. }| x).collect();
assert_eq!(x_coords, vec![1, 3]);
}
struct Point {
x: i32,
y: i32,
}
9.闭包与错误处理
闭包中的 Result 处理
rust
fn main() {
let result = try_operation(|| {
if true {
Ok(42)
} else {
Err("failed".to_string())
}
});
assert_eq!(result, Ok(84));
}
fn try_operation<F>(op: F) -> Result<i32, String>
where
F: FnOnce() -> Result<i32, String>,
{
op().map(|x| x * 2)
}
使用 ? 运算符
rust
fn main() -> Result<(), String>{
let nums = vec![1, 2, 3];
let processed = process_data(&nums, |x| {
if *x > 0 {
Ok(x * 2)
} else {
Err("Negative number".to_string())
}
})?;
println!("{:?}", processed);
Ok(())
}
fn process_data<F>(data:&[i32],f:F) -> Result<Vec<i32>,String>
where
F:Fn(&i32) -> Result<i32,String>,
{
data.iter().map(f).collect()
}
10.闭包与性能优化
避免闭包分配
rust
fn main() {
bad_perf();
good_perf();
}
fn bad_perf(){
let mut sum = 0;
for i in 0..1000{
let mut add = |x| sum += x;
add(i);
}
println!("sum (bad): {}", sum);
}
fn good_perf(){
let mut sum = 0;
let mut add = |x| sum += x;
for i in 0..1000 {
add(i);
}
println!("sum (good): {}", sum);
}
内联闭包
rust
// 编译器通常会内联简单的闭包
let nums = vec![1, 2, 3];
let sum: i32 = nums.iter().map(|x| x * 2).sum();
Rust 编译器(特别是 LLVM 优化器 )会对简单的闭包(如 |x| x * 2
)进行 内联优化(Inline Optimization):
- 内联 :编译器会将闭包的逻辑直接展开到调用处,而不是生成一个独立的函数调用。
- 例如,
map(|x| x * 2)
可能会被优化成类似for x in nums { x * 2 }
的循环。
- 例如,
- 零成本抽象 :迭代器和闭包在 Rust 中是零成本的,编译后的代码和手写的
for
循环性能几乎相同。 - 性能优势:避免了闭包的运行时开销(如虚函数调用)。
5.总结
Rust 的闭包是一个强大而灵活的特性,它:
- 允许简洁的匿名函数语法
- 可以捕获环境变量
- 通过 trait 系统 (
Fn
,FnMut
,FnOnce
) 提供灵活的调用语义 - 在迭代器、并发、回调等场景中有广泛应用
- 几乎零运行时开销,因为大多数情况下会被内联
理解闭包的工作原理和限制对于编写高效、优雅的 Rust 代码至关重要。通过合理使用闭包,可以大大简化代码并提高表达能力。