一句话总结
闭包是"能记住周围环境的匿名函数".
Rust 用 Fn,FnMut,FnOnce 三个特征,精确描述一个闭包对捕获变量做了什么--只读,要改,还是要吃掉.看懂这三者,就看懂了闭包的所有规则.
前置回顾
上一篇迭代器中,map,filter,fold 的参数都是闭包.本篇深入闭包本身:它如何捕获环境,编译器如何判断它属于哪一类,以及如何在函数签名中传递和返回它.
闭包是什么:带环境的函数
普通函数只能用自己的参数,闭包则能捕获定义它时所在作用域里的变量.
|
factor 既不是 multiply 的参数,也不是全局变量,却能在闭包体内使用--这就是"捕获环境".普通的 fn 函数做不到这一点,这是闭包与函数最本质的区别.
普通函数像一台标准化机器,只认投进去的原料(参数).
闭包则像一位带着自己工具箱上门的师傅,除了给的活儿,还能随手用上兜里早就装好的工具(捕获的变量).
三个特征:Fn / FnMut / FnOnce
闭包对捕获变量的使用方式有三个层级,Rust 用三个特征区分.编译器会根据闭包体内的行为自动推断它属于哪一类,通常不必手动标注.
| 特征 | 如何使用捕获变量 | 能否多次调用 | 类比 |
|---|---|---|---|
Fn |
只读借用 &T |
可任意多次 | 查阅工具箱,看完放回 |
FnMut |
可变借用 &mut T |
可多次(需 mut) |
使用并改动工具箱 |
FnOnce |
取走所有权 T |
只能一次 | 把工具箱整个送出去 |
|
三者是包含关系,不是平行关系
这三个特征层层嵌套:每个 Fn 闭包也是 FnMut,每个 FnMut 也是 FnOnce.
直觉上:能"只读使用"的,自然也满足"可变使用"和"消耗一次"的要求.因此函数参数若写 F: FnOnce(),约束最宽松,能接受全部三类闭包;写 F: Fn() 则最严格.定参数时按需选最宽松的那个.
捕获方式由"用法"决定
闭包默认以"够用就好"的最小权限捕获,能借用就借用,能只读就不可变.只有当闭包体内的操作要求更高权限时,捕获方式才升级.
|
move 关键字:强制夺取所有权
有时即使闭包体内只是读取,也想让它取得变量所有权--最典型的场景是把闭包交给新线程或返回出函数.这时用 move 强制改变捕获方式.
|
move 改变的是捕获方式,不是特征类别
这是高频误解.move 强制闭包按所有权捕获变量,但闭包属于 Fn / FnMut / FnOnce 中的哪一类,仍由闭包体如何使用这些变量决定.
例如 move || println!("{data:?}") 把 data 移了进来,但体内只是只读,所以它依然是 Fn,可以反复调用."移进来"和"消耗掉"是两回事.
闭包作为函数参数
把闭包传进函数有两种写法,各有取舍.
|
泛型 vs dyn 怎么选
泛型/impl Trait(静态分发):编译期为每种闭包生成专门代码,无运行时开销,是默认首选.
dyn Fn(动态分发):适合需要把不同闭包放进同一个容器(如 Vec<Box<dyn Fn()>>),或想避免泛型导致的代码膨胀时.
规则:能静态就静态,需要异质集合时才上 dyn.
返回闭包
从函数返回闭包是个经典难点,因为每个闭包都有独一无二,无法写出的匿名类型.解决办法是用 impl Trait 或 Box<dyn>.
|
返回闭包几乎总要 move
闭包捕获的是函数局部变量(这里是 n).函数返回后局部变量就销毁了,如果闭包还借用着它,就会留下悬垂引用--Rust 当然不允许.所以必须用 move 把 n 的所有权搬进闭包,让它随闭包一起存活.
若多个分支返回不同闭包,impl Trait 行不通(类型不一致),需改用 Box<dyn Fn(...)>.
闭包与函数指针的关系
不捕获任何环境的闭包,可以自动退化成普通函数指针 fn.函数指针同时实现了三个 Fn 特征,所以凡能接受函数指针处,也能传闭包.
|
快速回顾
- 闭包 = 匿名函数 + 捕获环境,这是它区别于
fn的本质 - 三特征:
Fn只读,FnMut可变,FnOnce消耗,呈包含关系 - 捕获方式由用法自动推断,按最小权限原则
- move 强制按所有权捕获,但不直接决定特征类别
- 参数优先静态分发(泛型/impl Trait),异质集合才用
dyn - 返回闭包用
impl Fn+move,多分支不同闭包用Box<dyn>
动手练习
- 三种捕获方式:写三个闭包,分别只读,修改,消耗一个捕获的
String,尝试各调用两次,观察哪个会编译失败及报错原因. - 返回闭包:实现
make_multiplier(n: i32) -> impl Fn(i32) -> i32,去掉move看编译器如何报错,理解为什么必须 move. - 动态分发:把若干个
Box<dyn Fn(i32) -> i32>放进一个Vec,依次对同一输入调用,体会动态分发的用途.