Skip to content

闭包

闭包是一种匿名函数,可以赋值给变量,也可以作为参数传递给其他函数。与普通函数不同的是,闭包能够捕获定义时所在作用域中的变量:

rust
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 函数名和括号,参数写在两条竖线之间。下面四种写法等价:

rust
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,无需让调用者知道类型。

类型一旦推导就被锁定,同一个闭包不能在不同调用中使用不同类型:

rust
let example_closure = |x| x;

let s = example_closure(String::from("hello")); // ✅ x 被推导为 String
let n = example_closure(5);                     // ❌ 尝试用 i32 调用,类型冲突

工作原理: 隐形结构体

闭包的本质是编译器自动生成的匿名结构体,捕获的变量成为结构体的字段,函数体通过实现 Fn/FnMut/FnOnce 特征来提供调用能力:

比如下面的闭包:

rust
let x = 10;
let add_x = |y| x + y;

编译器在幕后实际生成的是:

rust
// 编译器生成的匿名结构体(概念示意)
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将变量移入闭包

编译器根据闭包体中对变量的实际操作自动选择捕获方式,能用借用就不用所有权。

rust
// 不可变借用: 只读
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,可以强制闭包获取所有捕获变量的所有权,无论闭包体中是否真正需要:

rust
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 即此意):

rust
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

可变借用的方式捕获,可以修改外部变量,能被多次调用:

rust
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

不可变借用的方式捕获,不修改外部变量,能被多次调用,限制最严格:

rust
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
rust
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 时,传入 FnFnMut 的闭包都能通过。

实际开发中,建议先写 Fn,编译器会在需要时提示你应该改用 FnMut 还是 FnOnce

源码印证

标准库中三个特征的简化定义清晰地展示了这个继承关系:

rust
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 约束来接收闭包参数:

rust
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 特征,因此接受闭包参数的地方也可以传入普通函数:

rust
fn add_one(x: i32) -> i32 { x + 1 }
println!("{}", apply_to_5(add_one)); // ✅ 函数也能传入

在结构体中存储闭包

由于闭包类型不固定,在结构体中存储闭包需要用泛型参数标注类型约束:

rust
// 定义一个结构体,字段是一个闭包
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 语法(编译期确定具体类型,零开销):

rust
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> 装箱:

rust
// ❌ 错误: 两个分支的闭包类型不同(即使签名一样)
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)✅ 多种

基于 MIT 协议发布