甚么是旋量群 closure
旋量群(closure),是一类非官方函数,因此具备“捕捉”内部表达式的潜能。旋量群有时也称之为 lambda 函数。Rust中的旋量群,基本上句法如下表所示右图:
对以内旋量群,能看见,有三个模块,以三个|围困。继续执行句子包涵在 {} 中。旋量群的模块和codice类别选定,与一般函数的句法完全相同。而旋量群的模块和codice类别都是能略去的。因而,以内旋量群可略去为
跟一般函数那样,codice也能采用句子块函数顺利完成,与 return 句子的促进作用那样,因而以内旋量群可略去为
更近一步棋,假如旋量群的句子体只包涵两条句子,所以内层的四元组也能略去;假如有数条句子,则无法略去。以内旋量群能进一步略去为
closure看上去和一般函数很相近,不过,它事实上有很多差别。具体来说,closure能“捕捉”内部环境表达式,fn无法。示比如下表所示:
校对,结论再次出现校对严重错误:error: cant capture dynamic environment in a fn item; use the || { … } closure form instead [E0434]。由此看来,函数inner_add是无法出访变量x的。所以依照校对器的提示信息,他们改成旋量群试一试:
这一次,校对通过。
而对不需要捕捉环境表达式的场景,一般函数fn也能当成closure采用。
在这个示例中,map方法的签名是:
这里的 FnOnce 在下文中会详细解释。它在此处的含义是,f 是一个旋量群模块,类别为 FnOnce(T) -> U,依照上下文类别推导,事实上是 FnOnce(i32)->i32。他们定义了一个一般函数,签名为 fn(i32) -> i32,也那样能用于该模块中。假如他们用旋量群来写,这样也能:
一般函数和旋量群之间最大的区别是,一般函数无法捕捉环境表达式,在这个例子中,虽然他们的 multiply2 函数定义在 main 函数体内,但是它无权出访 main 函数内的局部表达式。其次,fn的采用能再次出现在定义位置之前,看起来像是先采用,再声明,这是没问题的。而相对而言,closure更像是一个能被“调用”的表达式。它具备和表达式同样的“生命周期”。
表达式捕捉
接下来他们研究一下 closure 背后的原理。Rust目前的closure实现,又叫做unboxed closure,它背后的原理与C++ 11的 lambda 非常相近。当一个closure创建的时候,校对器实质上帮他们生成了一个非官方struct,通过自动分析closure的内部逻辑,决定该结构体包括哪些数据,以及这些数据该如何初始化。
考虑以下例子:
他们来思考一下,假如不采用旋量群,自己来实现以内逻辑,该怎么做。
以内这个例子,他们模拟了一个旋量群的原理,事实上Rust校对器就是用类似的手法来处理旋量群句法的。对比一下采用旋量群句法的版本和他们手工实现的版本,他们能看见,创建旋量群的时候,就相当于创建了一个结构体,他们把需要捕捉的环境表达式存到这个结构体中。旋量群调用的时候,相当于调用了跟这个结构体相关的一个成员函数。
但是,还有几个问题没有解决,当校对器把旋量群句法糖转换为一般的类别和函数调用的时候:
结构体内部的成员,应该用甚么类别,如何初始化?应该用i32或是&i32还是&mut i32?call函数调用的时候self应该用甚么类别?应该写self或是&self还是&mut self?
理解了这三个问题的答案,就能完全理解了Rust的旋量群的原理。
关于第一个问题,Rust主要是通过分析内部表达式在旋量群中的使用方式,通过一系列的规则自动推导出来的。主要规则如下表所示:
假如一个内部表达式在旋量群中,只通过借用指针&采用,所以这个表达式就可通过引用&的方式捕捉;假如一个内部表达式在旋量群中,通过& mut指针采用过,所以这个表达式就需要采用&mut的方式捕捉;假如一个内部表达式中旋量群中,通过move的方式采用过,所以这个表达式就需要采用“by value”的方式捕捉。简单点总结,就是说规则是,在保证能校对通过的情况下,校对器会自动选择一类,对内部影响最小的类别存储。对被捕捉的类别为T的内部表达式,在非官方结构体中的存储方式选择为,尽可能先选择 &T 类别,其次选择 &mut T 类别,最后选择 T 类别。示比如下表所示:
对以内旋量群,捕捉了内部的三个表达式x y z。其中,y只通过&T的方式采用了;z通过&mut T的方式被采用了;x通过T的方式被采用了。因而,校对器会依照这些信息,自动生成结构类似这样的非官方结构体:
move关键字
以内表达式捕捉的规则都是针对只做为局部表达式的旋量群而准备的。而有些时候,他们的旋量群的生命周期可能会超过一个函数的范围。比如,他们能将此旋量群存储到某个数据结构中,在当前函数返回之后继续采用。这样一来,就可能再次出现更复杂的情况,在旋量群被创建的时候,它通过引用的方式捕捉了某些局部表达式,而在旋量群被调用的时候,它所指向的一些内部表达式已经被释放了。示比如下表所示:
大家能看见,函数make_adder中有一个局部表达式x,按照前面所述的规则,它被旋量群所捕捉,而且应该采用引用&的方式。而旋量群则作为了函数codice被传递出去了。于是,旋量群被调用的时候,它内部的引用所指向的内容,已经被释放了。这种情况,应该会再次出现典型的野指针问题,属于内存不安全的范畴。幸运的是,该程序在Rust中根本无法校对通过,严重错误信息为:error: closure may outlive the current function, but it borrowsx, which is owned by the current function [E0373]。信息提示信息非常清晰,他们又能感谢Rust帮他们发现了一个问题。
所以这种情况,他们应该怎么写才对呢?这里要介绍一个新的关键字 move,用于修饰一个 closure。示比如下表所示:
加上 move 关键字后,所有的表达式捕捉,全部采用by value的方式。也就是说,校对器生成的非官方结构体,内部看上去像是这样的:
所以,move 关键字能改变旋量群捕捉表达式的方式,一般用于旋量群需要传递到函数内部 (escaping closure) 的情况。
Fn/FnMut/FnOnce
内部表达式捕捉的问题解决了,他们再看看第二个问题,旋量群被调用的方式。他们注意到,旋量群被调用的时候,不需要继续执行某个成员函数,而是采用类似函数调用的句法来继续执行。这是因为它自动实现了校对器提供的几个特殊的 trait,Fn或者 FnMut 或者 FnOnce。
它的定义如下表所示:
这几个 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_onece(self, args:Args)函数。
示比如下表所示:
对上例,drop函数的签名是,fn drop<T>(_x: T),它接受的模块类别是T。因而,在旋量群中采用该函数,会导致内部表达式v通过move的方式捕捉。校对器为该旋量群自动生成的非官方类别类似这样:
对这样的一个结构体,他们来尝试一下实现一下FnMut trait:
当然,这是校对不过的,函数体内需要一个Self类别,但是函数模块只提供了&mut Self类别。因而,校对器不会为这个旋量群实现FnMut trait。唯一能实现的trait就只剩下了FnOnce。
这个旋量群被调用的时候,当然就会调用call_once这个方法。他们知道,fn call_once(self, arg:Args)这个函数调用的时候,self模块是move进入函数体的,会“吃掉”self表达式。在此函数调用后,这个旋量群的生命周期就结束了。所以,对FnOnce类别的旋量群,它只能被调用一次。FnOnce也是得名于此。他们自己试一下:
校对器在处理上面这段代码的时候,做了一个类似这样的展开:
同样的道理,他们试一试Fn的情况:
可以看见,上面这个旋量群捕捉的环境表达式,在采用的时候只需要&Vec<i32>类别即可。因而它能实现Fn trait。旋量群在被调用的时候,继续执行的是fn call(&self)函数。所以,调用多次也是没问题的。
他们假如给上面的程序添加上move关键字,也依然能通过:
能看见,move关键字,只是影响了环境表达式被捕获的方式。第三行,创建旋量群的时候,表达式v被move进入了旋量群中。第四行,旋量群调用的时候,依照推断规则,依然采用的fn call(&self)函数,因而旋量群表达式c能多次调用。
旋量群与泛型
他们已经知道,闭包背后是依靠trait来实现的。但是旋量群相关的trait跟其它的trait相比,句法上有特殊之处。比如说,他们想让旋量群作为一个模块,传递到函数中,能这么写:
其中泛型模块F的约束条件是F: Fn(i32) -> i32,这里Fn(i32) -> i32是针对旋量群设计的专门的句法,而不是像一般trait那样采用Fn<i32, i32>这个样子来写。除了句法之外,Fn FnMut FnOnce其它方面都跟一般的泛型一致。
请大家一定要注意的是,每个旋量群,校对器都会为它生成一个非官方结构体类别。即使三个旋量群的模块和codice一致,它也是 完全不同的三个类别,只是都实现了同一个trait而已。下面他们用一个示例演示:
校对,结论出错,严重错误信息为:
能看见,他们用同一个表达式来绑定三个旋量群的时候,发生了类别严重错误,请大家牢牢记住,不同的旋量群是不同的类别。
既然如此,跟一般的trait那样,假如他们需要向函数中传递旋量群,有两种方式:
通过泛型的方式。这种方式会为不同的旋量群模块类别生成不同版本的函数,实现静态分派。通过trait object的方式。这种方式会将旋量群box进入堆内存中,向函数传递一个胖指针,实现运行期动态分派。关于动态分派和静态分派的内容,将在下篇文章中详细说明。此处只做一个简单示比如下表所示:
假如他们希望一个旋量群作为函数的codice。所以就无法采用泛型的方式了。因为泛型类别不在模块中再次出现,而仅仅在返回类别中再次出现的话,会要求在调用的时候显式选定类别,校对器才能顺利完成类别推导。可是调用方根本无法选定具体类别,因为旋量群类别是非官方类别,用户无法显式选定。所以这样的写法是校对不过的:
所以,有人提出了一份 RFC,希望加入一个新的句法 fn test() -> impl Fn(i32)->i32,这样的句法糖在很多地方能简化代码的编写。不过这项改变需要考虑的问题太多,暂时还没能达成完全一致。所以,目前来说,唯一的方式就是把旋量群装箱进入堆内存中,采用Box< Fn(i32)->i32 >这种类别返回。
但是,这样做,又会有另外一个问题。那就是,Box 类别和 FnOnce 类别之间的配合不够默契。怎么回事呢?假如他们有一个 Box<FnOnce()> 这样的类别,调用它的时候,会发生校对严重错误。假设校对器为当前旋量群生成的非官方类别名字是 ClosureEnv,所以 Box<ClosureEnv> 类别在调用 call_once 方法的时候,self 的实际模块类型是 Box<Self>,而函数签名要求的形式模块类别却是 Self,会发生类别不匹配问题。
上面用 Box<Fn()> 的情况不会出问题,是因为这种情况下,依照自动 Deref 原则,实际模块类别是 Box<Self> 类别,而形式模块类别是 &Self 类别的话,能发生自动转换。所以不会有甚么问题。
为了处理这样的一个情况,Rust team 又临时性的设计了一个补丁 std::boxed::FnBox trait,专门用于处理这样的情况。假如你需要用 Box<FnOnce()> 类别,请暂时改用 Box<FnBox()> 类别。将来,把上面所说的这个问题比较好的解决掉之后,这个临时性的 trait 就会被删掉。
旋量群与生命周期
(高阶生命周期的例子有误,现删除,待以后补充)。
本文同步发布在微信公众号:Rust编程,欢迎关注。