Skip to content

22.3 Fn/FnMut/FnOnce

外部变量捕获的问题解决了,我们再看看第二个问题,闭包被调用的方式。我们注意到,闭包被调用的时候,不需要执行某个成员函数,而是采用类似函数调用的语法来执行。这是因为它自动实现了编译器提供的几个特殊的 trait,Fn 或者 FnMut 或者 FnOnce

注意:小写 fn 是关键字,用于声明函数;大写的 Fn 不是关键字,只是定义在标准库中的一个 trait。

它们的定义如下:

rust
pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

这几个 trait 的主要区别在于,被调用的时候 self 参数的类型。

  • FnOnce 被调用的时候,self 是通过 move 的方式传递的,因此它被调用之后,这个闭包的生命周期就已经结束了,它只能被调用一次,可以注意到 该 trait 被其他两个 trait 所继承并实现了;
  • FnMut 被调用的时候,self&mut Self 类型,有能力修改当前闭包本身的成员,甚至可能通过成员中的引用,修改外部的环境变量。这种闭包可以被多次调用。;
  • Fn 被调用的时候,self&Self 类型,只有读取环境变量的能力。这在并发调用闭包多次的情况下很重要。

那么,对于一个闭包,编译器是如何选择 impl 哪个 trait 呢?答案是,编译器会都尝试一遍,实现能让程序编译通过的那几个。 闭包调用的时候,会尽可能先选择调用fn call(&self, args: Args)函数,其次尝试选择fn call_mut(&mut self, args: Args)函数,最后尝试使用fn call_once(self, args: Args)函数。这些都是编译器自动分析出来的。

还是用示例来讲解比较清晰:

rust
fn main() {
    let v: Vec<i32> = vec![];
    let c = || std::mem::drop(v);
    c();
}

对于上例,drop函数的签名是fn drop<T>(_x: T),它接受的参数类型是 T。因此,在闭包中使用该函数会导致外部变量v通过 move 的方式被捕获。编译器为该闭包自动生成的匿名类型,类似下面这样:

rust
struct ClosureEnvironment {
    v: Vec<i32> // 这里不是引用
}

对于这样的结构体,我们来尝试实现 FnMut trait:

rust
impl FnMut<Vec<i32>> for ClosureEnvironment {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output {
        drop(self.v)
    }
}

当然,这是行不通的,因为函数体内需要一个 Self 类型,但是函数参数只提供了 &mut Self 类型。因此,编译器不会为这个闭包实现 FnMut trait。唯一能实现的 trait 就只剩下了 FnOnce

这个闭包被调用的时候,当然就会调用call_once方法。我们知道,fn call_once(self, arg: Args)这个函数被调用的时候,self 参数是 move 进入函数体的,会“吃掉” self 变量。在此函数调用后,这个闭包的生命周期就结束了。所以,FnOnce 类型的闭包只能被调用一次。FnOnce 也是得名于此。我们自己来试一下:

rust
fn main() {

    let v: Vec<i32> = vec![];
    let c = || drop(v); // 闭包使用捕获变量的方式,决定了这个闭包的类型。它只实现了`FnOnce trait`。
    c();
    c(); // 再调用一次试试,编译错误 use of moved value: `c`。`c`是怎么被 move 走的?
}

编译器在处理上面这段代码的时候,做了一个下面这样的展开:

rust
fn main() {
    struct ClosureEnvironment {
        _v: Vec<i32>
    }
    let v: Vec<i32> = vec![];
    let c = ClosureEnvironment { _v: v }; // v move 进入了 c 的成员中
    c.call_once(); // c move 进入了 call_once 方法中
    c.call_once(); // c 的生命周期已经结束了,这里的调用会发生编译错误
}

同样的道理,我们试试 Fn 的情况:

rust
fn main() {
    let v: Vec<i32> = vec![1,2,3];
    let c = || for i in &v { println!("{}", i); };
    c();
    c();
}

可以看到,上面这个闭包捕获的环境变量在使用的时候,只需要&Vec<i32>类型即可,因此它只需要捕获环境变量v的引用。因此它能实现 Fn trait。闭包在被调用的时候,执行的是fn call(&self)函数,所以,调用多次也是没问题的。

我们如果给上面的程序添加move关键字,依然可以通过:

rust
fn main() {
    let v: Vec<i32> = vec![1,2,3];
    let c = move || for i in &v { println!("{}", i); };
    c();
    c();
}

可以看到,move 关键字只是影响了环境变量被捕获的方式。第三行,创建闭包的时候,变量 v 被 move 进入了闭包中,闭包中捕获变量包括了一个拥有所有权的 Vec<i32>。第四行,闭包调用的时候,根据推断规则,它依然是 Fn 型的闭包,使用的是 fn call(&self) 函数,因此闭包变量 c 可以被多次调用。

参考

Released under the MIT License