闭包也是Rust中使用量很大的数据结构了,闭包的使用可以给程序的逻辑处理过程带来很多便利,但是也带来了不少的复杂性。Rust中的闭包类型有三种,这相对其他语言来说就已经十分复杂了,也让急于上手的人一时间摸不到头脑。本文尝试对闭包和语句块的部分特性做一些讨论。
本文是《Rust惑点启示笔记》系列文章中的第八篇。这个系列的文章主要计划对Rust语言使用过程中经常会出现的一些容易迷惑的惑点进行一个启发式的讨论和记录。
本系列专题还有以下文章:
- Rust惑点启示系列(一):避免随意使用Clone
- Rust惑点启示系列(二):从函数中返回一些东西
- Rust惑点启示系列(三):引用的生命期从来就不够长
- Rust惑点启示系列(四):到处都是的大括号
- Rust惑点启示系列(五):工具类型太多了
- Rust惑点启示系列(六):如何下手编写一个函数
- Rust惑点启示系列(七):使用全局变量和单例
- Rust惑点启示系列(八):奇形怪状的Rust闭包
不同类型的闭包
在学习完闭包以后,会知道闭包的类型有Fn
、FnMut
和FnOce
三种。
对于这三种类型的闭包,很多教程都是从闭包的特性角度去介绍的。但是往往会让人看着一脸懵。比如FnOnce
就被描述为会消耗掉所捕获的值的闭包,而且闭包本身也会被消耗掉;FnMut
的描述是包含可修改数据的闭包。那么仅看这些描述,能否在实际编码中正确的选择闭包的类型?
比如在闭包所处的环境中,既有不可变引用,也有可变引用,甚至还有可以转移所有权进入闭包的值,那么这个闭包需要选择什么类型呢?
其实看一下标准库的文档,可以看出来Fn
、FnMut
和FnOnce
之间的区别。对于Fn
,标准库文档中有这样一句描述:
Instances of
Fn
can be called repeatedly without mutating state.
我相信大多数教程可能更多的关注了这句话的前半部分,也就是Fn
是可以多次调用的。但是Fn
的另一个重要特性是这句话的后半句:不会修改状态。所以Fn
的特点是使用不可变引用捕获其上下文中的内容,这样才能保证Fn
闭包中是不会对上下文做出任何修改动作的,而且也不会把上下文中值的所有权转移进闭包里。
对于FnMut
,标准库文档中的描述是:
Instances of
FnMut
can be called repeatedly and may mutate state.
根据这句描述,FnMut
的确是一个会包含可修改数据的闭包。但是这句话的实际意义是FnMut
是会修改它所在的上下文环境的,因为FnMut
中是可以做出修改数据的操作的,也就是说FnMut
是可以以可变引用的方式捕获上下文内容的。
对于FnOnce
,标准库文档中的描述是:
Instances of
FnOnce
can be called, but might not be callable multiple times. Because of this, if the only thing known about a type is that it implementsFnOnce,
it can only be called once.
标准库的文档并没有很明确的说明FnOnce
具有如何捕获上下文的能力,但是这个描述的后面还有半句话:
FnOnce
is implemented automatically by closures that might consume captured variables,
注意这是半句话。这半句话提到了自动实现了FnOnce
的闭包可能会消费掉捕获到的变量,所以结合Fn
和FnMut
是扩展自FnOnce
(Fn
是扩展自FnMut
的)的,可以想到,FnOnce
会转移上下文中变量的所有权进入到闭包,所以这样才能导致FnOnce
只能够被调用一次。
所以这三个闭包的类型总结起来可形成了下面一个对比的表。
Fn |
FnMut |
FnOnce |
|
---|---|---|---|
调用能力 | 多次调用 | 多次调用 | 单次调用 |
捕获变量方式 | 不可变借用 | 可变借用 | 转移所有权 |
完成捕获的方式 | &T |
&mut T |
T |
对上下文的修改 | 无修改 | 有修改 | 消耗上下文 |
使用场景 | 无副作用的回调函数 | 需要改变上下文的回调函数 | 用于一次性操作任务或闭包 |
可接受替代实例 | 无 | Fn |
Fn 和FnMut |
Fn
是FnMut
的亚特征,所以在需要一个FnMut
的地方是可以传入Fn
实例,FnMut
是FnOnce
的亚特征,所以在需要一个FnOnce
的地方是可以传入FnMut
实例的,也是可以传入Fn
实例的。这个是比较好理解的,FnOnce
是会捕获上下文中变量的所有权的,但是FnMut
和Fn
捕获的都是可变引用和不可变引用,所需的访问权限小于所有权转移,所以FnOnce
的特性是可以满足FnMut
和Fn
的需要的。相同的道理也可以用在FnMut
和Fn
的关系上。
FnMut
中使用&T
捕获上下文中的变量,那么这个闭包的特性就跟Fn
是一样的,即便闭包的类型是FnMut
,也不会捕获变量的可变引用,也就是说FnMut
同时支持&T
和&mut T
两种捕获方式,但是实际使用的时候并不会额外捕获。这个原理在FnOnce
处也是适用的,即FnOnce
同时支持&T
、&mut T
和T
的捕获方式。
如何选择所需要使用的闭包类型
对于编码时闭包类型的选择,标准库文档中也给出了推荐。
Use
FnOnce
as a bound when you want to accept a parameter of function-like type and only need to call it once. If you need to call the parameter repeatedly, useFnMut
as a bound; if you also need it to not mutate state, useFn
.
这个推荐的意思是,如果你计划这个闭包只被调用一次,那么可以使用FnOnce
作为闭包参数类型的约束。如果需要重复调用这个闭包,那么可以使用FnMut
作为参数类型的约束。如果还不想让这个闭包修改上下文中的内容,那么可以使用Fn
作为约束。
标准库文档的推荐已经说的非常明确了。不过也可以根据上文中对照表中的对于上下文的修改能力来决定使用何种类型的闭包。原则上选择自己代码中所需权限最小的闭包类型即可。
很多闭包为什么要使用move
在很多调用闭包上可能会看到这样的语法:move || {}
。不少教程中的解释是显式声明闭包会转移上下文中变量的所有权。其实这个解释并不是很准确,如果看完了上面三种闭包类型的区别,应该可以想到,move
关键字是显式强制声明使用FnOnce
类型的闭包了。也就是说此时在闭包中使用的一定是所有权转移后的变量。
例如以下示例:
|
|
move
关键字的情况下,闭包会根据对于上下文变量的使用方式来决定对上下文变量的捕获方式。
语句块也可以使用move
?
语句块也是一个表达式,自然也可以捕获变量的所有权,所以使用move
来修饰一个语句块,也是显式声明语句块将会将所用到的变量的所有权转移到语句块中。
move
关键字很常见,尤其是在构建一个异步任务或者多线程任务时,因为此时需要确保所有必要的变量都被正确捕获并传输到异步任务或者线程中。
如何避免闭包捕获变量所有权
看到这里,如何避免闭包捕获变量所有权的方法就很显而易见了。以下几种方法在编码时如何选择取舍,需要看程序的需要。
- 尽量不要使用
move
关键字捕获变量的所有权。 - 尽量避免使用可能产生转移所有权的函数。
- 提前准备变量的引用计数智能指针类型,传递智能指针而不是变量。