闭包是将函数,或者说代码和其环境一起存储的一种数据结构。闭包引用的上下文中的 自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分,
官方定义:
闭包是一种匿名类型,一旦声明,就会产生一个新的类型,但这个类型无法被其它地方使 用。这个类型就像一个结构体,会包含所有捕获的变量。
第一部分:什么是闭包?(通俗概念)
1. 直觉理解
- 普通函数 (
fn) :像一个只有手艺的工匠。你给他什么材料(参数),他就加工什么。他记不住之前的状态,也不认识他工作台以外的东西。 - 闭包 (
Closure) :像一个自带背包的打工仔。他不仅有手艺(代码逻辑),他还随身背着一个背包,背包里装着他从定义他的环境中顺手拿走的数据(捕获变量)。
2. 为什么叫“闭包”?
因为它“封闭”了外部的变量。它不仅包含代码,还包含(捕获)了定义时的环境上下文。
第二部分:写法与语法(从繁到简)
闭包的语法深受 Ruby 和 Smalltalk 的影响,使用管道符 |。
Rust
fn main() {
let x = 1;
// 1. 普通函数(无法直接访问 x,除非传参)
fn function_add(val: i32) -> i32 { val + 1 }
// 2. 闭包:完整写法 (显式标注类型)
let closure_full = |val: i32| -> i32 { val + x };
// 3. 闭包:省略写法 (最常用)
// Rust 编译器极聪明,它能自动推断 val 和返回值的类型
let closure_simple = |val| val + x;
// 4. 闭包:连花括号都省了 (如果只有一行)
let closure_oneline = |val| val + x;
println!("{}", closure_oneline(5)); // 输出 6
}
关键点:注意上面代码中的 val + x。val 是参数,而 x 是从外面“捕获”进来的。普通函数 fn 做不到这一点。
第三部分:Fn, FnMut, FnOnce —— 闭包的灵魂
这是面试必考、开发必用的核心。Rust 根据闭包如何使用它背包里的东西,把闭包分成了三类。
这完全对应 Rust 的所有权三原则:
- 不可变借用 (
&T) -> 对应Fn - 可变借用 (
&mut T) -> 对应FnMut - 获取所有权 (
T) -> 对应FnOnce
1. Fn (只读的闭包)
- 行为:闭包只读取背包里的数据,不修改,也不拿走。
- 场景:可以被无限次调用。
- 比喻:打工仔只是拿出背包里的书看了一眼。
Rust
let s = String::from("hello");
// 只是打印 s,没有修改它,也没有把 s 卖掉
let print_it = || {
println!("I see: {}", s);
};
print_it(); // 可以调用多次
print_it();
2. FnMut (能修改的闭包)
- 行为:闭包会修改背包里的数据。
- 要求:调用闭包时,闭包变量本身必须是
mut的。 - 比喻:打工仔拿出背包里的笔记本,并在上面写了字。
Rust
let mut count = 0;
// 这里必须加 mut,因为闭包内部要改变 count 的状态
let mut inc = || {
count += 1;
println!("Count is: {}", count);
};
inc(); // count 变 1
inc(); // count 变 2
3. FnOnce (一次性的闭包)
- 行为:闭包把背包里的数据消耗掉了(比如 Move 走了所有权)。
- 后果:这个闭包只能被调用一次!因为它把数据“吃”了,第二次就没有数据可用了。
- 比喻:打工仔拿出背包里的苹果吃掉了。你不能让他再吃一次同一个苹果。
Rust
let s = String::from("hello");
let consume_it = || {
// 这里的 s 被 move 给了 into_bytes,所有权转移了
let _bytes = s.into_bytes();
};
consume_it();
// consume_it(); // 报错!苹果已经被吃掉了,不能再调一次
第四部分:底层规则(编译器干了什么?)
当你写出一个闭包时,Rust 编译器在背后悄悄生成了一个 struct(结构体)。
举个例子:
Rust
let x = 10;
let c = |y| x + y;
编译器在底层会把它转换成类似这样的代码:
Rust
// 1. 自动生成一个结构体,专门存捕获的变量
struct ClosureEnvironment {
x: i32, // 把外部的 x 存进来
}
// 2. 为这个结构体实现 trait(这里是 Fn)
impl Fn<(i32,)> for ClosureEnvironment {
extern "rust-call" fn call(&self, args: (i32,)) -> i32 {
self.x + args.0 // 真正的逻辑
}
}
// 3. 实例化这个结构体
let c = ClosureEnvironment { x: 10 };
结论:
- 闭包本质上就是一个结构体实例(保存数据) + Trait 实现(保存代码逻辑)。
- 这就解释了为什么闭包可以存状态。
- 如果你没有捕获任何变量,闭包就退化成了一个普通的函数指针。
第五部分:普通函数 fn 和闭包 Fn 的关系
这经常让人混淆。注意大小写:
fn(小写) :函数指针类型。它没有背包,不捕获环境。Fn(大写) :这是一个 Trait(特征)。
关系规则:
- 普通函数也实现了
Fn,FnMut,FnOnce。 因为普通函数不需要捕获环境,所以它既可以被当做“看一眼”用,也可以被当做“一次性”用(虽然它不消耗啥)。 - 你可以把普通函数传给需要闭包的地方。
Rust
// 这个函数接收一个闭包(或者实现了 Fn 的东西)
fn call_me<F>(f: F) where F: Fn() {
f();
}
fn my_function() { println!("我是普通函数"); }
fn main() {
let my_closure = || println!("我是闭包");
call_me(my_closure); // 传闭包,没问题
call_me(my_function); // 传普通函数,也没问题!
}
第六部分:move 关键字(强制拿走背包)
默认情况下,闭包是“能借就借”(引用)。但有时候,你需要强制闭包把外部变量的所有权拿走。
场景:当你把闭包返回出去,或者传给一个新的线程时。因为主线程可能结束了,栈内存被释放,如果是借用的引用,那就悬空了。
Rust
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
// thread::spawn 要求闭包必须拥有数据的所有权
// 因为新线程可能活得比 main 函数久
thread::spawn(move || {
// 加了 move,list 被彻底移动到了这个闭包里
println!("From thread: {:?}", list);
}).join().unwrap();
// println!("{:?}", list); // 报错!list 已经被 move 走了
}
第七部分:总结与继承关系
为了方便记忆,请记住这个金字塔关系:
-
FnOnce (最宽容) :只要能被调用一次就行。
- 所有闭包都实现了
FnOnce。
- 所有闭包都实现了
-
FnMut (中间) :能被调用多次,且允许修改状态。
- 如果一个闭包实现了
FnMut,它一定也自动实现了FnOnce。
- 如果一个闭包实现了
-
Fn (最严格) :能被调用多次,且不修改状态(纯读)。
- 如果一个闭包实现了
Fn,它一定也自动实现了FnMut和FnOnce。
- 如果一个闭包实现了
小白怎么选?
-
当你在写函数,需要接收一个闭包作为参数时:
- 如果你的函数里只调它一次 -> 用
FnOnce(给调用者最大的自由)。 - 如果你要调多次,且可能改数据 -> 用
FnMut。 - 如果你要调多次,且不干扰数据 -> 用
Fn。
- 如果你的函数里只调它一次 -> 用
一句话总结:闭包就是 匿名函数 + 捕获的环境数据(struct)。