Rust 闭包(Closure)完整详解
涵盖 :闭包基础语法、三种捕获方式、Fn/FnMut/FnOnce trait、move 语义等
重点:捕获规则自动判断、闭包与函数指针区别、线程异步场景应用
目录
- 闭包是什么
- 三种捕获环境的方式(核心重点)
- 闭包的三种 trait(Fn / FnMut / FnOnce)(#三闭包的三种 traitfn--fnmut--fnonce)
- move 闭包与引用生命周期坑(#四 move-闭包与引用生命周期坑)
- 闭包作为返回值
- 捕获混合变量:部分 move(#六捕获混合变量部分 move)
- [闭包 vs 普通函数指针 fn](#闭包 vs 普通函数指针 fn "#%E4%B8%83%E9%97%AD%E5%8C%85-vs-%E6%99%AE%E9%80%9A%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88-fn")
- move 闭包 + 内部可变(结合 RefCell)(#八 move-闭包内部可变结合 refcell)
- 高频易错点
- 速记口诀
一、闭包是什么
闭包是匿名可执行函数,可以捕获当前作用域的变量,不需要提前命名,常用于回调、迭代器适配器、延迟执行。
语法格式
rust
// 基础:|参数 | 表达式
let add = |a, b| a + b;
// 多行用大括号
let calc = |x| {
let t = x * 2;
t - 1
};
// 调用
println!("{}", add(1, 2));
对比普通函数
| 类型 | 说明 |
|---|---|
| 函数 | fn add(a:i32,b:i32)->i32 {a+b},不能捕获外部变量 |
| 闭包 | ` |
类型推导规则
闭包不强制标注参数/返回类型,编译器自动推导;仅无法推导时才需要手动标注:
rust
let f = |x: i32| -> i32 { x + 1 };
二、三种捕获环境的方式(核心重点)
闭包捕获外部变量分三种模式,编译器自动根据使用情况选择:
- 不可变借用 &T:只读取变量
- 可变借用 &mut T:修改变量
- 获取所有权 move:转移变量到闭包内
1. 不可变借用捕获(只读)
只读取外部变量,不修改、不转移所有权,原变量仍可用:
rust
let s = String::from("hello");
let read = || println!("{}", s); // 捕获 &String
read();
println!("{}", s); // s 正常可用
2. 可变借用捕获(修改)
闭包内部修改外部变量,外部变量必须 mut:
rust
let mut num = 0;
let mut inc = || num += 1; // &mut num
inc();
println!("{}", num); // 1
3. move 强制获取所有权
关键字 move:强制把捕获变量的所有权转移进闭包,外部变量失效。
适用场景:闭包需要长期存活(线程、异步),生命周期长于原变量。
rust
let s = String::from("data");
let consume = move || println!("{}", s); // s 所有权移入闭包
consume();
// println!("{}", s); // 编译报错,s 已 move
典型场景:线程
线程生命周期独立,必须 move 把数据移入闭包:
rust
use std::thread;
let msg = String::from("thread msg");
let handle = thread::spawn(move || {
println!("{}", msg);
});
handle.join().unwrap();
捕获规则自动判断总结
| 使用方式 | 捕获模式 |
|---|---|
| 仅读 | &T 不可变借用 |
| 内部修改 | &mut T 可变借用 |
使用 move 关键字 |
强制转移所有权 |
| 同时读 + 修改 | 可变借用 |
三、闭包的三种 trait(Fn / FnMut / FnOnce)
所有闭包自动实现以下其中一个或多个 trait,区分依据是捕获方式、调用次数:
1. FnOnce
| 特性 | 说明 |
|---|---|
| 调用次数 | 只能调用一次 |
| 捕获方式 | 获取所有权 move,内部消耗变量(如 String) |
| 行为 | 调用后内部资源被 drop,无法二次执行 |
2. FnMut
| 特性 | 说明 |
|---|---|
| 调用次数 | 可调用多次 |
| 捕获方式 | &mut T |
| 要求 | 闭包变量本身需要声明 mut |
3. Fn
| 特性 | 说明 |
|---|---|
| 调用次数 | 可无限次调用 |
| 捕获方式 | &T,无任何修改 |
| 行为 | 只读取捕获变量,不修改、不获取所有权 |
优先级兼容关系
rust
Fn ⊂ FnMut ⊂ FnOnce
Fn自动实现FnMut+FnOnceFnMut自动实现FnOnce
函数参数约束闭包类型
rust
// 接收只读闭包 Fn
fn run_fn<F: Fn()>(f: F) { f(); f(); }
// 接收可修改捕获的闭包 FnMut
fn run_mut<F: FnMut()>(mut f: F) { f(); f(); }
// 仅能执行一次的闭包 FnOnce
fn run_once<F: FnOnce()>(f: F) { f(); }
迭代器适配器全部接收 Fn / FnMut
rust
let nums = vec![1, 2, 3];
// map 接收 Fn
let double: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter 接收 Fn
let even: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// fold 累加闭包 FnMut
let sum = nums.iter().fold(0, |acc, &x| acc + x);
四、move 闭包与引用生命周期坑
- 不加 move 捕获引用:闭包生命周期绑定外部变量,外部销毁则闭包失效;
- move 捕获所有权:数据归属闭包,不受外部生命周期限制,适合跨线程;
- move 不会强制拷贝,只是转移所有权(Move 语义);如果捕获 Copy 类型(i32),会直接复制一份值。
rust
// Copy 类型 move 只是拷贝,外部变量仍可用
let a = 10;
let f = move || println!("{}", a);
f();
println!("{}", a); // 正常,i32 实现 Copy
五、闭包作为返回值
方案 1:impl Trait(静态分发,单一闭包类型)
函数只能返回同一种闭包,性能无开销:
rust
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add5 = make_adder(5);
println!("{}", add5(3)); // 8
方案 2:Box<dyn Fn()>(动态分发,支持多分支返回不同闭包)
闭包尺寸不固定,装箱放入堆,胖指针返回:
rust
fn select_fun(flag: bool) -> Box<dyn Fn(i32) -> i32> {
if flag {
Box::new(|x| x * 2)
} else {
Box::new(|x| x / 2)
}
}
两种方案对比
| 方案 | 说明 |
|---|---|
| impl Trait | 静态分发,单一闭包类型,性能无开销 |
| Box | 动态分发,支持多分支返回不同闭包,有运行时开销 |
六、捕获混合变量:部分 move
Rust 闭包捕获是粒度为整个变量,无法只 move 结构体某个字段。
如果只想转移部分字段,手动解构提前移动:
rust
struct Data {
name: String,
id: i32,
}
let d = Data { name: "test".into(), id: 1 };
// 提前把 name 移出去,闭包只 move name,id 借用
let name = d.name;
let f = move || println!("{}", name);
七、闭包 vs 普通函数指针 fn
1. fn 函数指针
| 特性 | 说明 |
|---|---|
| 捕获能力 | 不能捕获任何外部环境,无上下文 |
| 类型写法 | fn(i32)->i32 |
| 存储形式 | 轻量,仅地址 |
2. 闭包
| 特性 | 说明 |
|---|---|
| 捕获能力 | 可捕获环境变量,携带上下文 |
| 类型写法 | 无法直接写成简单 fn 类型,只能用 impl Fn / Box<dyn Fn> |
| 底层实现 | 匿名结构体:存储所有捕获的变量 |
相互转换:无捕获闭包可转为 fn
不捕获任何外部变量的闭包,能自动转函数指针:
rust
let f = |a: i32| a + 1;
let fp: fn(i32) -> i32 = f;
八、move 闭包 + 内部可变(结合 RefCell)
move 拿走所有权,但需要内部修改,配合 RefCell 内部可变性:
rust
use std::cell::RefCell;
use std::rc::Rc;
let cnt = Rc::new(RefCell::new(0));
let c = Rc::clone(&cnt);
let f = move || {
*c.borrow_mut() += 1;
};
f();
println!("{}", cnt.borrow());
多线程场景 :替换为
Arc<Mutex<T>>。
九、高频易错点
⚠️ 以下易错点需特别注意
- 迭代器传入修改外部变量的闭包 ,闭包必须
mut; - 不加 move 的闭包无法传入线程,生命周期不匹配编译报错;
- 返回多个不同闭包不能用 impl Trait ,必须
Box<dyn Fn>; - 闭包捕获是整变量捕获,不能单独捕获结构体某个成员;
- move 对 Copy 类型是复制,非 Copy 类型是所有权转移;
- FnOnce 闭包只能调用一次,第二次调用编译报错;
- 闭包自动推导类型,复杂场景类型推导失败需手动标注参数类型。
十、速记口诀
rust
闭包匿名函数,竖线包裹参数;
自动捕获外部变量,三种借用模式自动选;
move 强制转移所有权,线程异步必备;
Fn 只读可多次,FnMut 可修改捕获;
FnOnce 仅执行一次,三者包含关系记心间;
impl Trait 静态返回单一闭包,Box dyn 支持多分支;
无捕获闭包可转 fn 指针,捕获环境不能转;
迭代器 map/filter 全用闭包,延迟执行简洁高效。