Rust 闭包:捕获环境的魔法函数
今天来聊聊 Rust 语言中一个非常强大且有趣的特性:闭包 (Closures)。你可能在其他编程语言听说过闭包这个词,简单来说,闭包就像一个"加强版"的函数,不仅能执行代码,还能"记住"并访问定义时所处的环境中的变量。

什么是闭包?
通常我们所说的闭包,在计算机科学中指的是词法闭包 (Lexical Closure)。你可以把它想象成一个函数,这个函数自带了一个"小背包",里面装着出生时(定义时)那个环境里的一些东西,特别是那些在函数内部没有定义但被用到的变量。
- 外部环境:这就是闭包被定义时所处的作用域。
- 自由变量:在函数内部使用,但既不是函数的参数也不是在函数内部定义的变量,就叫做自由变量。在函数式编程中,我们也会听到这个术语。所以闭包可以是自备装备参与战斗。
核心概念: 闭包就是将自由变量和自身"绑定"在一起的函数。当闭包被创建时,它会捕获(capture)它周围环境中的变量,这样即使在定义闭包的环境结束后,这个闭包仍然可以访问和操作这些被捕获的变量。

rust
fn counter(i: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |n: i32| n + i)
}
fn main(){
let f = counter(2);
assert_eq!(3, f(1));
}
在这个例子中,counter
函数返回了一个闭包。这个闭包是 |n: i32| n + i
。注意,这个闭包用到了变量 i
,但 i
并不是闭包的参数,也不是在闭包内部定义的。i
就是一个自由变量,它来自 counter
函数的作用域。当我们在 main
函数中调用 counter(2)
时,闭包就捕获了当时 i
的值 2
。之后,当我们调用 f(1)
时,实际上执行的是 1 + 2
,所以结果是 3
。
闭包的特点:不仅仅是函数
闭包之所以强大,是因为它具备以下这些特性:
- 加强版的函数: 没错,闭包本质上仍然是函数,很多时候我们用匿名函数的形式来创建闭包。
- Rust 的语法糖: 在 Rust 中,闭包的语法设计得非常简洁,用
|| {}
的形式就能快速创建一个匿名函数。这背后其实是 Rust 编译器帮我们做了很多工作。 - 捕获环境中的自由变量: 这是闭包最核心的特性。普通函数只能访问传递给它的参数和函数内部定义的变量,而闭包可以访问其定义时环境中的变量。
- 绑定自由变量: 正如我们之前所说,闭包会将它用到的自由变量和自身绑定在一起。
- 可以添加变量: 虽然原文没有展开,但闭包本身也可以拥有自己的局部变量。
- 作为参数或返回值: 闭包可以像普通函数一样,作为其他函数的参数传递,也可以作为函数的返回值。这使得我们可以编写更加灵活和高阶的函数。
- 引用环境中的变量方式灵活: 闭包如何引用其环境中的变量取决于变量在闭包内的使用方式,这涉及到所有权和借用的概念,我们稍后会详细讨论。
- 不是函数指针而是 trait: 这是一个很重要的技术细节。在 Rust 中,闭包并不是像 C 语言那样的简单函数指针。每个闭包都有其独特的类型,它们实现了
Fn
、FnMut
或FnOnce
这三个 trait 之一,这决定了闭包如何捕获和使用环境中的变量。
闭包的基本语法
Rust 中闭包的基本语法是这样的:
rust
|parameters| -> return_type {
// 闭包体
}
parameters
: 闭包接收的参数列表,可以为空。参数类型通常可以由编译器自动推断。return_type
: 闭包的返回值类型,通常也可以由编译器自动推断。如果闭包体只有一个表达式,{}
可以省略。{}
: 闭包体,包含要执行的代码。
例如:
rust
let add_one = |x: i32| x + 1;
let print_hello = || println!("Hello!");
let multiply = |a, b| a * b; // 参数类型和返回值类型可以省略,由编译器推断
闭包的参数可以是任意类型
这和普通函数一样,闭包的参数可以是 Rust 支持的任何数据类型。
相同定义(同构)的闭包却属于不同类型
文章中提到了一个有趣的现象:
rust
fn main(){
let c1 = || {};
let c2 = || {};
let _v = [c1, c2]; // 报错!
}
这里会报错,因为 c1
和 c2
虽然看起来定义相同(都是没有参数也没有返回值的闭包),但它们实际上是不同的类型。每个闭包实例都有其自己独特的匿名类型。这与泛型不同,即使两个闭包的行为完全一样,它们的类型也是不一样的。
模拟编译器对闭包的实现
文章中给出了一个模拟编译器如何实现闭包的例子:
rust
#![feature(unboxed_closures)]
#![feature(fn_traits)]
struct Adder {
a: u32
}
impl FnOnce<(u32, )> for Adder {
type Output = u32;
extern "rust-call" fn call_once(self, b: (u32, )) -> Self::Output {
self.a + b.0
}
}
fn main() {
let adder = Adder { a: 3 };
assert_eq!(adder(2), 5);
}
这个例子通过自定义一个结构体 Adder
并为其实现 FnOnce
trait,来模拟一个捕获了外部变量 a
的闭包。当调用 adder(2)
时,实际上是调用了 call_once
方法,这个方法使用了 Adder
结构体中存储的 a
值和传入的参数 b.0
进行计算。
SUGGESTION: 可以更详细地解释 FnOnce
、(u32, )
这个元组参数、extern "rust-call"
调用约定以及 self
的含义,帮助读者理解闭包在底层是如何通过结构体和 trait 来实现的。
非装箱 (Non-Boxing)
Rust 闭包的一个重要特点是非装箱。这意味着:
- 更好的控制优化: 编译器可以更直接地操作闭包捕获的变量,避免了额外的堆分配和间接访问,从而实现更好的性能优化。
- 支持按值和按引用绑定环境变量: 闭包可以灵活地选择如何捕获其环境中的变量,可以是直接复制值(move),也可以是借用引用(borrow)。
- 支持 3 种不同的闭包访问
self
、&self
和&mut self
: 这与 Rust 的所有权和借用规则紧密相关,对应于FnOnce
(消耗所有权)、Fn
(不可变借用)和FnMut
(可变借用)这三个闭包 trait。
Fn
闭包:可以多次调用
实现了 Fn
trait 的闭包可以对其捕获的环境进行不可变借用 (&self
)。这意味着闭包在执行时不会修改它捕获的变量,因此可以安全地被多次调用。
rust
fn main(){
let greeting = String::from("Hey!");
let fn_closure = || {
println!("Closure says: {}", greeting);
};
fn_closure();
fn_closure(); // 可以多次调用
println!("{}", greeting); // greeting 的所有权仍然在 main 函数中
}
在这个例子中,fn_closure
借用了 greeting
字符串的引用,所以它可以多次打印 greeting
的值,而不会影响 greeting
本身的所有权。
FnMut
闭包:可以修改捕获的变量
实现了 FnMut
trait 的闭包可以对其捕获的环境进行可变借用 (&mut self
)。这意味着闭包在执行时可以修改它捕获的变量。
rust
fn main(){
let mut counter = 0;
let mut fn_mut_closure = || {
counter += 1;
println!("Counter is now: {}", counter);
};
fn_mut_closure(); // Counter is now: 1
fn_mut_closure(); // Counter is now: 2
println!("Final counter: {}", counter); // Final counter: 2
}
这里,fn_mut_closure
可变地借用了 counter
变量,每次调用闭包都会修改 counter
的值。注意闭包本身需要声明为 mut
,因为它会修改其捕获的环境。
FnOnce
闭包:只能调用一次
实现了 FnOnce
trait 的闭包会获取其捕获变量的所有权 ,或者将捕获的变量移动 (move) 到闭包内部 。这意味着闭包在执行后,其捕获的变量可能不再有效,因此 FnOnce
闭包只能被调用一次。
rust
fn main(){
let a = Box::new(23);
let call_me = || {
let c = a; // 将 a 的所有权移动到闭包内部
println!("Value: {}", c);
// a 在这里已经失效
};
call_me();
// call_me(); // 再次调用会报错,因为闭包已经消耗了 a 的所有权
}
在这个例子中,闭包 call_me
获取了 a
的所有权。一旦 call_me()
被调用,a
的所有权就转移到了闭包内部的 c
变量,外部的 a
变得无效。因此,我们不能再次调用 call_me()
。
使用 move
关键字自动实现 FnOnce
如果你希望闭包获取其环境中所有变量的所有权,可以使用 move
关键字:
rust
fn main() {
let name = String::from("Alice");
let move_closure = move || {
println!("Name: {}", name);
// name 的所有权已经移动到闭包内部
};
move_closure();
// println!("{}", name); // 报错!name 的所有权已经转移
}
使用 move
关键字创建的闭包通常会实现 FnOnce
trait,因为变量的所有权被移动到了闭包内部。
作为参数
闭包可以作为函数的参数传递,这使得我们可以编写更加灵活和通用的函数。通常我们会使用泛型和 trait bounds 来接受闭包作为参数:
rust
fn call_me<F: Fn()>(f: F) {
f();
}
fn function() {
println!("I'm a function!");
}
fn main() {
let closure = || println!("I'm a closure!");
call_me(closure); // 传递闭包
call_me(function); // 也可以传递普通函数,因为函数也实现了 Fn trait
}
在这个例子中,call_me
函数接受一个实现了 Fn()
trait 的参数 f
。这意味着任何没有参数也没有返回值的闭包(或者函数)都可以传递给 call_me
。
作为返回值
闭包也可以作为函数的返回值。由于闭包有其独特的匿名类型,直接返回一个具体的闭包类型是不可行的。通常我们会使用 impl Trait
语法来返回一个实现了特定闭包 trait 的类型:
rust
fn create_fn() -> impl Fn() {
let text = "Fn".to_owned();
move || println!("This is a: {}", text)
}
fn create_fnmut() -> impl FnMut() {
let mut text = "FnMut".to_owned();
move || text.push_str(" - Modified");
}
fn create_fnonce() -> impl FnOnce() {
let text = "FnOnce".to_owned();
move || println!("This is a: {}", text)
}
fn main() {
let fn_plain = create_fn();
fn_plain(); // This is a: Fn
let mut fn_mut = create_fnmut();
fn_mut();
fn_mut();
let fn_once = create_fnonce();
fn_once();
// fn_once(); // 再次调用会报错
}
在这个例子中,create_fn
返回一个实现了 Fn()
trait 的闭包,create_fnmut
返回一个实现了 FnMut()
trait 的闭包,create_fnonce
返回一个实现了 FnOnce()
trait 的闭包。注意我们使用了 move
关键字将 text
的所有权移动到闭包内部,这样即使 create_fn
函数结束,闭包仍然可以访问 text
。

总结
闭包是 Rust 中一个非常强大且灵活的特性,通过闭包我们创建可以捕获和操作其定义环境中变量的匿名函数。理解 Fn
、FnMut
和 FnOnce
这三个闭包 trait,以及闭包如何捕获变量(通过引用或移动所有权),对于编写高效且符合 Rust 所有权和借用规则的代码至关重要。掌握闭包,你就能写出更加简洁、优雅且富有表达力的 Rust 代码!
希望这篇文章能够帮助你更好地理解 Rust 中的闭包。如果你有任何问题或想进一步探讨,欢迎留言交流!