闭包

6 阅读6分钟

闭包是将函数,或者说代码和其环境一起存储的一种数据结构。闭包引用的上下文中的 自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分,

官方定义:

闭包是一种匿名类型,一旦声明,就会产生一个新的类型,但这个类型无法被其它地方使 用。这个类型就像一个结构体,会包含所有捕获的变量

第一部分:什么是闭包?(通俗概念)

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 + xval 是参数,而 x 是从外面“捕获”进来的。普通函数 fn 做不到这一点。


第三部分:Fn, FnMut, FnOnce —— 闭包的灵魂

这是面试必考、开发必用的核心。Rust 根据闭包如何使用它背包里的东西,把闭包分成了三类。

这完全对应 Rust 的所有权三原则

  1. 不可变借用 (&T) -> 对应 Fn
  2. 可变借用 (&mut T) -> 对应 FnMut
  3. 获取所有权 (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(特征)。

关系规则:

  1. 普通函数也实现了 FnFnMutFnOnce。 因为普通函数不需要捕获环境,所以它既可以被当做“看一眼”用,也可以被当做“一次性”用(虽然它不消耗啥)。
  2. 你可以把普通函数传给需要闭包的地方

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 走了
}

第七部分:总结与继承关系

为了方便记忆,请记住这个金字塔关系

  1. FnOnce (最宽容) :只要能被调用一次就行。

    • 所有闭包都实现了 FnOnce
  2. FnMut (中间) :能被调用多次,且允许修改状态。

    • 如果一个闭包实现了 FnMut,它一定也自动实现了 FnOnce
  3. Fn (最严格) :能被调用多次,且不修改状态(纯读)。

    • 如果一个闭包实现了 Fn,它一定也自动实现了 FnMut 和 FnOnce

小白怎么选?

  • 当你在写函数,需要接收一个闭包作为参数时

    • 如果你的函数里只调它一次 -> 用 FnOnce(给调用者最大的自由)。
    • 如果你要调多次,且可能改数据 -> 用 FnMut
    • 如果你要调多次,且不干扰数据 -> 用 Fn

一句话总结:闭包就是 匿名函数 + 捕获的环境数据(struct)。