闭包(Closures)
本教程共 78 篇 · 第 48 篇 · 更新于 2026-08-08 · 约 11 分钟阅读
本节目标:理解闭包是能捕获环境的匿名函数,掌握它的语法与类型推导,弄清它和普通函数的区别,以及 Fn/FnMut/FnOnce 三种捕获方式。
闭包这名字听着玄,其实就一句话:能”顺手”用到周围变量的匿名函数。它和函数像,但比函数多一项本事——捕获作用域里的值。
1-1 闭包是什么
看个小例子就懂:
fn main() {
let x = 1;
let sum = |y| x + y; // 闭包捕获了外面的 x
assert_eq!(3, sum(2)); // 1 + 2 = 3
}
sum 是个闭包,它接收参数 y,却用到了作用域里的 x。调用 sum(2) 时,把参数 2 和捕获的 1 相加。这就是”闭包”:能赋值给变量、能当参数传、还能捕获调用者作用域里的值。
1-2 闭包语法
闭包用 || 包住参数,{} 包住函数体。参数写法 |参数|,多个就 |a, b|:
|param1, param2| {
语句1;
返回表达式
}
只有一行返回表达式时,可以省略花括号和 return:
let sum = |x, y| x + y; // 最后一行表达式就是返回值
对比一下函数和闭包:
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 ;
第一行是函数,后三行是闭包,形式很接近。闭包体最后一行(没加分号)的值,就是返回值。
Note
let action = || {...};是把”闭包本身”赋给action,不是调用结果。所以要像函数一样用action()去调用它;只有action()才执行。
1-3 类型推导:推一次就定了
函数必须标参数和返回值类型,因为函数是给外人用的 API。闭包一般不对外,所以能享受编译器的类型推导,不用标类型。但注意:推导出来后类型就固定了,不能又传这种又传那种。
let example_closure = |x| x;
let s = example_closure(String::from("hello")); // 推导出 x 是 String
let n = example_closure(5); // 报错:之前推成 String 了,5 不兼容
第一次用 String 调用,编译器把 x 定为 String;第二次传 5 就类型不符。这和泛型不同——泛型每种类型各生成一份代码,闭包只锁一个类型。
1-4 闭包能捕获环境,函数不能
这是闭包最独特的能力。函数定义在哪都访问不到外面的局部变量:
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x } // 报错:函数抓不到 x
equal_to_x(4);
}
同样逻辑用闭包就成:
fn main() {
let x = 4;
let equal_to_x = |z| z == x; // 闭包捕获了 x
assert!(equal_to_x(4));
}
编译器甚至会提示你”用闭包替代函数”。捕获环境要额外分配内存存这些值,所以函数没有这负担,闭包有——这是它的代价。
1-5 三种 Fn 特征:怎么捕获
闭包捕获变量有三种方式,对应三种 Fn 特征:
FnOnce:拿走捕获变量的所有权,所以只能调用一次;FnMut:以可变借用捕获,能修改被捕获的值;Fn:以不可变借用捕获,只能读不能改。
一个闭包实现哪种,取决于它怎么用捕获的变量,不是怎么捕获的。规则是:所有闭包都至少实现 FnOnce;没移走所有权的还实现 FnMut;不需要改值的还实现 Fn。所以只读的闭包三种都算,最灵活。
看 FnMut 的例子,闭包修改了外面的 s:
fn main() {
let mut s = String::new();
let mut update = |str: &str| s.push_str(str); // 捕获可变借用
update("hello");
println!("{:?}", s);
}
因为闭包内部要改 s,所以闭包本身得声明成 mut update(注意:这是语言层的 mut 修饰符,和 FnMut 特征名不是一回事,但常搭配使用)。
1-6 move 关键字
默认情况下,闭包按”借用”的方式捕获变量。若想强制闭包拿走所有权,在参数前加 move:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("向量是:{:?}", v);
});
handle.join().unwrap();
}
move 常用于闭包生命期比捕获变量长时(比如扔给另一个线程)。闭包拿了 v 的所有权,线程才能安全地用。
Tip一个闭包实现了哪种 Fn,取决于它怎么使用捕获变量,而不取决于是否写了
move。move只决定”怎么捕获”(拿所有权),用法照旧。
1-7 闭包作函数参数
闭包常作为参数传给别的函数。因为每种闭包类型都不同,无法用具体类型标注,得用 Fn 系列 trait 做约束:
fn apply<F>(f: F)
where
F: Fn(i32) -> i32,
{
println!("{}", f(5));
}
fn main() {
let double = |x| x * 2;
apply(double); // 10
}
F: Fn(i32) -> i32 表示:参数 f 是个闭包,接收 i32、返回 i32。Fn 系列 trait 不仅对闭包有效,对普通函数也有效,所以传具名函数也行。
Warning想用闭包当返回值,直接用
Fn(...)作返回类型会报错(trait 没有固定大小)。要用impl Fn(...)或Box<dyn Fn(...)>。这块进阶内容先留个印象,知道”返回闭包得用impl Fn或Box”即可。
1-8 闭包作为返回值与 Fn 特征推断
闭包可以被当作值传来传去,但要把它作为函数返回值就有个坑:闭包的类型是”匿名”的,你写不出它的名字。于是返回闭包时,通常得把它装进 Box,写成 Box<dyn Fn(...) -> ...>(动态分发):
fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + n)
}
编译器会根据闭包”怎么捕获环境变量”自动推断它实现 Fn、FnMut 还是 FnOnce:只是借用就用 Fn;需要修改就用 FnMut;把变量”吃掉”(move 进闭包且不再归还)就用 FnOnce。你一般不用手写这些 bound,让编译器推即可,只有在把闭包当参数/返回值时才显式标注。
Tip记不住三者区别时,这样想:
Fn只读环境、FnMut改环境、FnOnce消耗环境。绝大多数读环境的闭包都是Fn,最省心。
1-9 闭包如何捕获环境
闭包之所以比函数强,是因为它能”抓走”周围作用域里的变量。捕获方式分三种,对应前面说的 Fn / FnMut / FnOnce:
- 只借不可变引用(只读):闭包实现
Fn,最省心,且往往能Copy; - 借可变引用(要改):实现
FnMut,调用时会改动外部变量; - 把变量”搬”进自己体内(move 所有权):实现
FnOnce,调用后变量就归闭包所有了。
let mut total = 0;
let mut add = |x: i32| { total += x; }; // 捕获 total 的可变引用 -> FnMut
add(3);
add(5);
println!("{total}"); // 8
多数情况下让编译器自动推断即可;只有当你要把闭包当参数或返回值时,才需要显式写出 Fn / FnMut / FnOnce 约束。
Note为什么有的闭包能
Copy、有的不能?关键看它怎么捕获:只读借用最轻量,可以到处拷贝;一旦动了或吞了外部变量,闭包就带上了”状态”,通常不能再简单复制。
1-10 小结
- 闭包是能捕获环境的匿名函数,语法
|参数| 表达式; - 闭包类型可推导,但推导后类型固定,不能混用;
- 闭包能捕获周围变量,普通函数不行;
- 三种捕获方式:
FnOnce(拿所有权)、FnMut(可变借用)、Fn(不可变借用); move强制闭包拿走捕获变量的所有权;- 闭包作参数用
Fn系列 trait 约束;作返回值用impl Fn或Box<dyn Fn>。
下一章看迭代器,它是闭包的最佳搭档,能让你用声明式的方式处理集合。