Appearance
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
可以被多次调用。