外部变量捕获的问题解决了,我们再看看第二个问题,闭包被调用的方式。我 们注意到,闭包被调用的时候,不需要执行某个成员函数,而是采用类似函数调用 的语法来执行。这是因为它自动实现了编译器提供的几个特殊的trait,Fn或者 FnMut或者FnOnce。 注意:小写fn是关键字,用于声明函数;大写的Fn不是关键字,只是定义在标 准库中的一个trait。 它们的定义如下: pub trait FnOnce { type Output; extern “rust-call” fn call_once(self, args: Args) -> Self::Output; } pub trait FnMut : FnOnce { extern “rust-call” fn call_mut(&mut self, args: Args) -> Self::Output; } pub trait Fn : FnMut { extern “rust-call” fn call(&self, args: Args) -> Self::Output; } 这几个trait的主要区别在于,被调用的时候self参数的类型。FnOnce被调用的时 候,self是通过move的方式传递的,因此它被调用之后,这个闭包的生命周期就已 经结束了,它只能被调用一次;FnMut被调用的时候,self是&mut Self类型,有能 力修改当前闭包本身的成员,甚至可能通过成员中的引用,修改外部的环境变量; Fn被调用的时候,self是&Self类型,只有读取环境变量的能力。 目前这几个trait还处于unstable状态。在目前的稳定版编译器中,我们不能针对 自定义的类型实现这几个trait,只能在nightly版本中开启#![feature(fn_traits)]功 能。 那么,对于一个闭包,编译器是如何选择impl哪个trait呢?答案是,编译器会 都尝试一遍,实现能让程序编译通过的那几个。闭包调用的时候,会尽可能先选择 调用fn call(&self,args:Args)函数,其次尝试选择fn call_mut(&self,args: Args)函数,最后尝试使用fn call_once(self,args:Args)函数。这些都是编译器 自动分析出来的。 还是用示例来讲解比较清晰: fn main() { let v: Vec = vec![]; let c = || std::mem::drop(v); c(); } 对于上例,drop函数的签名是fn drop(_x:T),它接受的参数类型是T。 因此,在闭包中使用该函数会导致外部变量v通过move的方式被捕获。编译器为该 闭包自动生成的匿名类型,类似下面这样: struct ClosureEnvironment { v: Vec // 这里不是引用 } 对于这样的结构体,我们来尝试实现FnMut trait: impl FnMut 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也是得名于此。我们自己来试一下: fn main() { let v: Vec = vec![]; let c = || drop(v); // 闭包使用捕获变量的方式,决定了这个闭包的类型。它只实现了FnOnce trait。 c(); c(); // 再调用一次试试,编译错误 use of moved value: c。c是怎么被move走的? } 编译器在处理上面这段代码的时候,做了一个下面这样的展开: fn main() { struct ClosureEnvironment { _v: Vec } let v: Vec = vec![]; let c = ClosureEnvironment { _v: v }; // v move 进入了c的成员中 c.call_once(); // c move 进入了 call_once 方法中 c.call_once(); // c 的生命周期已经结束了,这里的调用会发生编译错误 } 同样的道理,我们试试Fn的情况: fn main() { let v: Vec = vec![1,2,3]; let c = || for i in &v { println!(“{}”, i); }; c(); c(); } 可以看到,上面这个闭包捕获的环境变量在使用的时候,只需要&Vec类 型即可,因此它只需要捕获环境变量v的引用。因此它能实现Fn trait。闭包在被调 用的时候,执行的是fn call(&self)函数,所以,调用多次也是没问题的。 我们如果给上面的程序添加move关键字,依然可以通过: fn main() { let v: Vec = vec![1,2,3]; let c = move || for i in &v { println!(“{}”, i); }; c(); c(); } 可以看到,move关键字只是影响了环境变量被捕获的方式。第三行,创建闭包 的时候,变量v被move进入了闭包中,闭包中捕获变量包括了一个拥有所有权的 Vec。第四行,闭包调用的时候,根据推断规则,它依然是Fn型的闭包,使用 的是fn call(&self)函数,因此闭包变量c可以被多次调用。

推荐链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: