一句话总结
闭包(closure)是能捕获周围变量的匿名函数.它根据"如何使用捕获的变量"自动归入三个 trait:Fn(只读借用),FnMut(可变借用),FnOnce(取得所有权).
move 关键字强制闭包夺取捕获变量的所有权,是把数据交给线程/任务的关键.
闭包基础
第 09 篇里 |&n| n * 2 就是闭包.它像匿名函数,但更简洁,且能捕获定义处周围的变量:
|
Rust 闭包约等于 Go 的 func(x int) int { return x * factor },但写法精简到 |x| x * factor,参数和返回类型多数可推断,无需显式标注.
"捕获周围变量"这点两者一致.
捕获:闭包如何"记住"环境
闭包能访问定义它的作用域里的变量,这叫捕获.关键问题是:它以什么方式捕获?--是借用,还是夺走所有权?Rust 的规则是"按需捕获":编译器根据闭包体内如何使用变量,自动选择最宽松的方式.
|
三个 trait:Fn,FnMut,FnOnce
正因捕获方式不同,闭包被自动归类为三种 trait 之一.这决定了它能被调用几次,能传给哪些接口:
| Trait | 捕获方式 | 能调用几次 | 直觉 |
|---|---|---|---|
Fn |
不可变借用 &T |
多次 | 只读取捕获的变量 |
FnMut |
可变借用 &mut T |
多次 | 会修改捕获的变量 |
FnOnce |
取得所有权 T |
仅一次 | 消耗捕获的变量 |
三者是包含关系,不是互斥
它们层层包含:能当 Fn 用的闭包,也满足 FnMut 和 FnOnce;能当 FnMut 用的也满足 FnOnce.
所以接收 FnOnce 的接口最宽容(什么闭包都收),接收 Fn 的最严格(只收只读闭包).设计接收闭包的函数时,约束选最宽松的那个即可.通常不必手动标注这些 trait--编译器会自动推断闭包属于哪类.
move:强制夺取所有权
默认按需捕获(能借用就借用).但有时需要闭包夺取捕获变量的所有权--最典型的是把闭包交给一个比当前作用域活得更久的地方(新线程,异步任务).这时用 move:
|
承上启下:move 是多线程的主角
为什么需要 move?设想把闭包传给一个新线程--线程可能比创建它的函数活得久,如果闭包只是借用外部变量,等原变量被释放,闭包里的引用就悬空了.
move 让闭包带走数据的所有权,从而能独立存活.在「Rust 进阶 · 生命周期与并发」分类里,thread::spawn(move || ...) 会是天天写的模式.
闭包作为函数参数
把闭包当参数,是迭代器适配器(map,filter)以及大量 API 的基础.用泛型 + trait 约束接收:
|
返回闭包
函数也能返回闭包.由于闭包的具体类型无法写出(每个闭包都是独一无二的匿名类型),返回时用 impl Fn(...):
|
为什么返回闭包要 move
make_adder 返回后,局部变量 n 本会被释放.若闭包只借用 n,返回的闭包就会持有悬垂引用.
move 把 n 的所有权搬进闭包,闭包带着自己的 n 离开函数--这与上面"交给线程"是同一个道理:闭包要活得比定义它的作用域久,就得拥有数据.
闭包 vs 普通函数
普通函数(函数指针 fn)不能捕获环境,闭包可以.当一个 API 接收闭包时,也可以传一个不捕获任何变量的普通函数--它能自动适配:
|
快速回顾
- 闭包:能捕获周围变量的匿名函数,语法
|参数| 表达式,类型多可推断. - 按需捕获:编译器据用法自动选择不可变借用/可变借用/取所有权.
- 三个 trait:
Fn(只读)包含FnMut(改)包含FnOnce(消耗);接口约束选最宽松者. - move:强制夺取捕获变量所有权,用于把闭包交给线程/任务或从函数返回.
- 作为参数/返回值:参数用
F: Fn(...)或impl Fn(...);返回用impl Fn(...)(常配 move). - 与普通函数:函数不能捕获环境,但可传给接收闭包的接口.
动手练习
- Fn 与 FnMut:写一个捕获外部变量的只读闭包和一个修改外部变量的闭包,分别对应
Fn与FnMut. - move 验证:用
move把一个Vec移入闭包,验证移动后原变量不可用. - 泛型接收闭包:写函数
apply<F: Fn(i32)->i32>(f: F, x: i32),传入闭包和普通函数各调用一次. - 返回闭包:写一个
make_multiplier(n),返回impl Fn(i32)->i32,解释为何需要move. - 迭代器中的闭包:对一个向量用
iter().filter(闭包).map(闭包).collect(),体会闭包作为迭代器适配器参数的用法.