Rust 闭包类型
闭包在多种语言中都存在,可以理解为当函数中可以定义另一个函数,而这个「另一个函数」有权读写包含它的函数中的变量时,它就成为了闭包。
在大多使用垃圾回收机制的语言中,闭包并不需要做什么特殊的考虑,但是在 Rust 中,因为所有权的存在,闭包类型可能算初学者的第一个堪。
类型定义
Rust 闭包的类型是编译期间创建的匿名类型,我们只能使用 trait 来与它进行交互。Rust 存在三种闭包 trait:Fn,FnMut 和 FnOnce,类型定义如下:
pub trait Fn<Args>: FnMut<Args>
where
Args: Tuple,
{
// Required method
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args>
where
Args: Tuple,
{
// Required method
extern "rust-call" fn call_mut(
&mut self,
args: Args
) -> Self::Output;
}
pub trait FnOnce<Args>
where
Args: Tuple,
{
type Output;
// Required method
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
通过类型定义我们可以注意到两件事情
它们所定义的方法接受的
self
类型不同Fn 的 call 方法要求接受的是
&self
类型FnMut 的 call_mut 方法要求接受的是
&mut self
类型FnOnce 的 call_once 方法 要求接受的是
self
类型
它们三者之间存在 supertrait 的关系
FnOnce 是 FnMut 的 supertrait
FnMut 是 Fn 的 supertrait
另外特别注意官方文档中对于它们的描述(使用的是 may 而不是 must)
Instances of FnOnce can be called, but might not be callable multiple times.
Instances of FnMut can be called repeatedly and may mutate state.
基础概念
闭包结构体
闭包在 Rust 中可以认为其实是一个动态生成的结构体然后实现了 Fn/FnMut/FnOnce 的 trait
struct Closure1 {
var1: T1,
var2: T2,
// ...
}
impl FnOnce for Closure1 {
fn call_once(self, args: Args) {
// ...
}
}
所以对于 Fn/FnMut/FnOnce 三个闭包 trait 而言
self 指的是闭包本身及它捕获的外部参数
args 是调用这个闭包函数时传递给它的参数
self
对于接收 self 作为参数的函数,针对每个独立的 self 该函数只能调用一次(调用以后 self 消失)
对于接收 &self 或 &mut self 作为参数的函数,针对每个独立的 self 可以调用多次该函数
如果函数接收了 &mut self 作为参数,那么对于是可以修改 self 的,修改的结果在函数外可见
根据 self 的不同用法,可以得知对于闭包的使用者选择闭包的定义时
调用 FnOnce 的 call_once 方法时,只能调用闭包函数一次,然后这个闭包就「消失」了不能再调用了
调用 Fn 的 call 方法或 FnMut 的 call_mut 方法时,可以调用多次
(注:我们通常不会手动调用上述这几个方法,直接写 f(arg1, arg2, ...)
这样 rust 会自动根据我们的 trait 类型帮我们转换成对相应方法的调用)
supertrait
Rust 的 supertrait 类似于面向对象语言中的接口的继承
如果 IA 是 IB 的 supertrait(即定义 trait IB: IA
)
定义类型 X 满足 IB 时,需要确保它同时满足 IA
接受 IA 作为参数时,同样可以接受 IB
对应到 Fn / FnMut / FnOnce 上来
接受 FnOnce 作为参数时,同样可以给它传递 Fn / FnMut
接受 FnMut 作为参数时,同样可以给它传递 Fn
使用
定义函数使用闭包时
当我们撰写函数需要接收闭包时,根据我们对于这个闭包的使用情况来选择 trait
如果我们只需要简单的调用这个闭包一次,那么使用 FnOnce
如果我们需要多次调用这个闭包,那么使用 FnMut
如果我们需要多次调用这个闭包且不希望它修改状态,那么使用 Fn
其实这个规则,说简单点就是「能使用 FnOnce 就用它,用不了就用 FnMut,还不行再用 Fn」
这可能稍微有点反直觉(至少我觉得是),重点就在于 FnOnce 的含义不是只能调用一次,而是可能无法被多次调用 —— 所以接收 FnOnce 意味着「外部可以传递任何函数进来」+「我保证只会调用这个函数一次」
例如我们常用的 Option::and_then、OnceCell::get_or_init 都只会调用所传入的闭包一次,因此使用 FnOnce 作为接收参数类型
而到 FnMut / Fn 上,可以说作为闭包的使用方,我们真正所在意的并不是是否修改闭包,而是所有权 —— &self 和 &mut self 的主要区别在于前者可以存在多个而后者只能存在一个(注意这里的 self 指的是闭包本身),因此,通常情况下我们使用的都是 FnMut,只有并发和极少数情况下我们才会用到 Fn。
我们常用的闭包,例如 Iterator::map 等需要多次调用我们闭包函数的,都是接收的 FnMut
而 Fn 通常被用在需要并发调用闭包函数的情况下(这就要求闭包不能修改自己的状态,否则不满足所有权规则),例如 Iterator::map 的并发版本 rayon::ParallelIterator::map 会并发调用我们的闭包函数,因此接收的是 Fn。
定义闭包时
闭包的类型通常不是我们选择的,而是编译器根据我们闭包内的代码的行为所推断的。
编译器在推断时,会尽可能让我们的闭包可用更广泛,因此,它会优先看我们的闭包是否可以推断成 Fn、无法的话试着推断为 FnMut、 最后还不行再推断成 FnOnce。
(注:根据 supertrait 的原则,推断成 Fn 意味着它同时满足 Fn、FnMut、FnOnce,推断成 FnMut 意味着它同时满足 FnMut 和 FnOnce,而推断成 FnOnce 则意味着它只满足 FnOnce)
编译器主要依赖我们闭包中怎么使用捕获的外部参数而推断类型,在我们自己利用 || {}
语法定义闭包时
如果完全不存在捕获的外部变量 → Fn
如果捕获了外部变量的引用,但是只读 → Fn
如果捕获了外部变量的可变引用,且修改了 → FnMut
如果捕获了外部变量的所有权(且不是 Copy)→ FnOnce
当然,在捕获多个变量时,将选择最强的约束,即需要所有捕获行为都满足 Fn 整体才能是 Fn、任何一个只是 FnOnce 则整体就只能是 FnOnce
另外,Rust 为了让我们更明确的知道什么时候转移了所有权,对于需要转移所有权的场景需要我们明确指出,而这个明确指出一般除了作为返回值返回就只有添加 move 关键字(以 move || {}
定义闭包)了;不过我们基本不用太过于关心这一条,当你需要转移所有权而没有明确指出时,编译器会告诉你的。
推荐阅读