闭包
闭包是一种匿名函数,可以赋值给变量,也可以作为参数传递给其他函数。与普通函数不同的是,闭包能够捕获定义时所在作用域中的变量:
fn main() {
let x = 1;
let sum = |y| x + y; // 闭包捕获了外部变量 x
assert_eq!(3, sum(2));
}这里 x 不是 sum 的参数,但闭包可以直接使用它。普通函数做不到这一点(把函数定义在 main 内部,也无法访问 x),编译器会报错 can't capture dynamic environment in a fn item。
闭包语法
闭包用 || 代替 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 ; // 省略花括号(单表达式)闭包通常无需手动标注参数和返回值类型,编译器根据上下文推导。与函数不同,闭包不作为公开 API,无需让调用者知道类型。
类型一旦推导就被锁定,同一个闭包不能在不同调用中使用不同类型:
let example_closure = |x| x;
let s = example_closure(String::from("hello")); // ✅ x 被推导为 String
let n = example_closure(5); // ❌ 尝试用 i32 调用,类型冲突工作原理: 隐形结构体
闭包的本质是编译器自动生成的匿名结构体,捕获的变量成为结构体的字段,函数体通过实现 Fn/FnMut/FnOnce 特征来提供调用能力:
比如下面的闭包:
let x = 10;
let add_x = |y| x + y;编译器在幕后实际生成的是:
// 编译器生成的匿名结构体(概念示意)
struct Closure { x: i32 }
impl Fn<(i32,)> for Closure {
fn call(&self, args: (i32,)) -> i32 { self.x + args.0 }
}
impl FnMut<(i32,)> for Closure { /* ... */ }
impl FnOnce<(i32,)> for Closure { /* ... */ }这就是为什么每个闭包都有独一无二的匿名类型——即使两个闭包的参数和返回值签名完全相同,它们的类型也不同,因为底层是不同的结构体。
捕获环境变量
闭包捕获外部变量有三种方式,与函数参数的三种传入方式一一对应:
| 捕获方式 | 对应参数方式 | 行为 |
|---|---|---|
| 不可变借用 | &T | 只读取变量值 |
| 可变借用 | &mut T | 修改变量值 |
| 获取所有权 | T | 将变量移入闭包 |
编译器根据闭包体中对变量的实际操作自动选择捕获方式,能用借用就不用所有权。
// 不可变借用: 只读
let x = 5;
let print_x = || println!("{}", x);
// 可变借用: 需要修改
let mut count = 0;
let mut inc = || count += 1;
// 获取所有权: 内部消耗了值
let s = String::from("hello");
let consume_s = || drop(s); // drop 获取所有权move 关键字
在参数列表前加 move,可以强制闭包获取所有捕获变量的所有权,无论闭包体中是否真正需要:
use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("{:?}", v); // v 的所有权被移入新线程的闭包
});
handle.join().unwrap();跨线程场景必须使用 move: 新线程的生命周期可能比当前作用域更长,如果只是借用,借用可能在线程结束前就失效了。
move控制的是如何捕获(转移所有权),而不影响闭包实现哪种 Fn 特征。实现哪种 Fn 特征取决于如何使用被捕获的变量。
三种 Fn 特征
三种捕获方式对应三种 Fn 特征。Rust 根据闭包如何使用被捕获变量,自动决定实现哪些特征:
FnOnce
获取了捕获变量的所有权,导致变量被消耗,因此只能调用一次(Once 即此意):
fn apply_once<F: FnOnce()>(f: F) {
f(); // 第一次调用,f 的所有权被消耗
// f(); // ❌ 不能再调用
}
let s = String::from("hello");
let consume = move || {
let s1 = s; // 将 s 移出,只能发生一次
println!("{}", s1);
};
apply_once(consume);FnMut
以可变借用的方式捕获,可以修改外部变量,能被多次调用:
fn apply_mut<F: FnMut()>(mut f: F) {
f();
f(); // ✅ 可以多次调用
}
let mut s = String::new();
let mut update = || s.push_str("hello");
apply_mut(update);
println!("{}", s); // "hellohello"注意: 闭包变量本身需要声明为 mut,因为调用时需要修改其内部捕获的可变引用。
Fn
以不可变借用的方式捕获,不修改外部变量,能被多次调用,限制最严格:
fn apply<F: Fn()>(f: F) {
f();
f(); // ✅ 可以多次调用
}
let s = String::from("hello");
let print = || println!("{}", s); // 只读取 s
apply(print);
println!("{}", s); // ✅ s 仍然有效三者的继承关系
三种 Fn 特征存在继承关系,从约束最严格到最宽松:
Fn ⊆ FnMut ⊆ FnOnce这里的继承是"约束的继承", 而不是"能力的扩张",实现了Fn的约束也就满足了FnMut和FnOnce的约束,所以实现了Fn就同时实现了FnMut和FnOnce
具体规则:
- 所有闭包都自动实现
FnOnce(至少能调用一次) - 没有移出捕获变量所有权的闭包,还自动实现
FnMut - 只需要不可变访问捕获变量的闭包,还自动实现
Fn
let s = String::new();
let print = || println!("{}", s); // 只读,实现全部三种
fn exec_once<F: FnOnce()>(f: F) { f() }
fn exec_mut<F: FnMut()>(mut f: F) { f() }
fn exec<F: Fn()>(f: F) { f() }
exec_once(print); // ✅ Fn 的闭包可以用于 FnOnce 约束
exec_mut(print); // ✅ Fn 的闭包可以用于 FnMut 约束
exec(print); // ✅ Fn 的闭包可以用于 Fn 约束这意味着: 当函数参数要求 FnOnce 时,传入 Fn 或 FnMut 的闭包都能通过。
实际开发中,建议先写
Fn,编译器会在需要时提示你应该改用FnMut还是FnOnce。
源码印证
标准库中三个特征的简化定义清晰地展示了这个继承关系:
pub trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output; // 消耗 self
}
pub trait FnMut<Args>: FnOnce<Args> { // 要求先实现 FnOnce
fn call_mut(&mut self, args: Args) -> Self::Output; // 借用 &mut self
}
pub trait Fn<Args>: FnMut<Args> { // 要求先实现 FnMut
fn call(&self, args: Args) -> Self::Output; // 借用 &self
}闭包作为参数
由于每个闭包都有独一无二的类型,无法用具体类型标注,通常使用泛型 + Trait 约束来接收闭包参数:
fn apply_to_5<F: Fn(i32) -> i32>(f: F) -> i32 {
f(5)
}
let double = |x| x * 2;
println!("{}", apply_to_5(double)); // 10函数指针(fn 类型)实现了全部三种 Fn 特征,因此接受闭包参数的地方也可以传入普通函数:
fn add_one(x: i32) -> i32 { x + 1 }
println!("{}", apply_to_5(add_one)); // ✅ 函数也能传入在结构体中存储闭包
由于闭包类型不固定,在结构体中存储闭包需要用泛型参数标注类型约束:
// 定义一个结构体,字段是一个闭包
struct Cacher<T>
where
T: Fn(u32) -> u32, // 闭包必须实现 Fn(u32) -> u32 的特征
{
query: T,
value: Option<u32>,
}
impl<T: Fn(u32) -> u32> Cacher<T> {
// 构造函数,接受一个闭包作为参数
fn new(query: T) -> Self {
Cacher { query, value: None }
}
// 调用闭包并缓存结果
fn value(&mut self, arg: u32) -> u32 {
match self.value {
// 如果已经有缓存值,直接返回
Some(v) => v,
// 没有缓存值,调用闭包计算并缓存结果
None => {
let v = (self.query)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
let mut c = Cacher::new(|x| x + 1);
println!("{}", c.value(2)); // 3
println!("{}", c.value(4)); // 3,缓存生效,不再调用闭包
}闭包作为返回值
函数返回闭包时,不能直接写 -> Fn(i32) -> i32,因为 Trait 没有固定大小,编译器在编译期无法确定返回值的栈空间。
单一类型: 使用 impl Fn 语法(编译期确定具体类型,零开销):
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8多种类型: impl Fn 要求所有分支返回同一具体类型,不同分支返回不同闭包时需要用 Box<dyn Fn> 装箱:
// ❌ 错误: 两个分支的闭包类型不同(即使签名一样)
fn make_op(add: bool) -> impl Fn(i32) -> i32 {
if add { |x| x + 1 } else { |x| x - 1 }
}
// ✅ 正确: 用 Box<dyn Fn> 包裹 Trait 对象
fn make_op(add: bool) -> Box<dyn Fn(i32) -> i32> {
if add {
Box::new(|x| x + 1)
} else {
Box::new(|x| x - 1)
}
}| 返回方式 | 语法 | 分发方式 | 能返回不同类型 |
|---|---|---|---|
impl Fn(...) | 编译期推导 | 静态(零开销) | ❌ 只能一种 |
Box<dyn Fn(...)> | Trait 对象 | 动态(vtable) | ✅ 多种 |