Rust 闭包类型

闭包在多种语言中都存在,可以理解为当函数中可以定义另一个函数,而这个「另一个函数」有权读写包含它的函数中的变量时,它就成为了闭包。

在大多使用垃圾回收机制的语言中,闭包并不需要做什么特殊的考虑,但是在 Rust 中,因为所有权的存在,闭包类型可能算初学者的第一个堪。

类型定义

Rust 闭包的类型是编译期间创建的匿名类型,我们只能使用 trait 来与它进行交互。Rust 存在三种闭包 trait:FnFnMutFnOnce,类型定义如下:

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; }

通过类型定义我们可以注意到两件事情

  1. 它们所定义的方法接受的 self 类型不同

    1. Fn 的 call 方法要求接受的是 &self 类型

    2. FnMut 的 call_mut 方法要求接受的是 &mut self 类型

    3. FnOnce 的 call_once 方法 要求接受的是 self 类型

  2. 它们三者之间存在 supertrait 的关系

    1. FnOnce 是 FnMut 的 supertrait

    2. 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_thenOnceCell::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 || {} 定义闭包)了;不过我们基本不用太过于关心这一条,当你需要转移所有权而没有明确指出时,编译器会告诉你的。

推荐阅读

Rust 标准库中使用到 Fn 的函数

判别 Fn、FnMut、FnOnce 的标准