rust语言基础学习: 闭包
2020-07-28
今天来学习rust中的闭包。
rust中的闭包实际上是一个匿名函数,这个匿名函数可以被赋值给一个变量,可以作为参数传递给函数,可以作为函数返回值被返回,也可以为它实现某个trait,使其表现出其他行为。 在rust中可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算。不同于函数,闭包允许捕获调用者作用域中的变量。
1.闭包的定义 #
在rust的reference中给出了闭包的定义:
- 闭包是由闭包表达式产生的匿名类型,每声明一个闭包都产生一个唯一的、匿名的类型。
- 可以将闭包理解成一种特殊的数据结构,闭包周围的作用域被称为其环境,闭包将其代码和环境存储在一起。
- 闭包中包含了它所捕获的其环境中的变量。
闭包定义的具体链接地址是https://doc.rust-lang.org/reference/types/closure.html。
rust会为每个闭包生成一个唯一的,新的匿名类型,这个匿名类型在内存中的排布和结构体挺相似的。
例1:
1fn f<F: FnOnce() -> String>(g: F) {
2 println!("{}", g());
3}
4
5fn main() {
6 let mut s = String::from("foo");
7 let t = String::from("bar");
8
9 f(|| {
10 s += &t;
11 s
12 });
13 // Prints "foobar".
14}
上面例1第9~12行声明了一个闭包,并将其作为参数传给f函数,调用了f函数。
例1中产生的闭包类型大致和下面例2的伪代码中的Closure结构体相似。
例2:
1fn f<F: FnOnce() -> String>(g: F) {
2 println!("{}", g());
3}
4
5struct Closure<'a> {
6 s: String,
7 t: &'a String,
8}
9
10impl<'a> FnOnce<()> for Closure<'a> {
11 type Output = String;
12 fn call_once(self) -> String {
13 self.s += &*self.t;
14 self.s
15 }
16}
17
18fn main() {
19 let mut s = String::from("foo");
20 let t = String::from("bar");
21
22 f(Closure { s: s, t: &t });
23}
例1中的比高代码相当于,例2中的伪代码,闭包类型相当于一个结构体,其所捕获的变量,闭包所捕获的变量相当于结构体中的字段。 闭包的代码相当于结构体对三个Fn trait的实现代码。闭包相关的三个Fn trait后边会介绍。
2.闭包类型占用内存的大小 #
如果把闭包理解成一个结构体,闭包所捕获的变量相当于结构体中的字段,则闭包的大小就和闭包参数,闭包代码的局部变量都没有关系,而只与闭包从其环境中捕获的变量有关。 下面我们尝试打印一下不同的闭包占用内存大小。
例3:
1use std::mem::size_of_val;
2
3fn main() {
4 // 闭包1, 没有参数, 没有捕获任何变量
5 // 闭包1相当于 struct{}
6 let c1 = || {};
7 println!("size_of_val(&c1) = {}", size_of_val(&c1)); // size_of_val(&c1) = 0
8
9 // 闭包2, 有1个参数, 没有捕获任何变量, 闭包的代码中有一个i32的局部变量num
10 // 闭包2相当于 struct{}
11 let c2 = |x: i32| {
12 let num = 3;
13 num * x
14 };
15 println!("size_of_val(&c2) = {}", size_of_val(&c2)); // size_of_val(&c2) = 0
16
17 let num: i32 = 3;
18 let x: i32 = 4;
19 // 闭包3, 没有参数, 捕获了其环境中的num和x两个i32变量的引用,即&num和&x
20 // 闭包3相当于 struct{num: &i32, x: &i32}
21 let c3 = || num * x;
22 println!("size_of_val(&c3) = {}", size_of_val(&c3)); // size_of_val(&c3) = 16
23
24 let num: i32 = 3;
25 let x: i32 = 4;
26 // 闭包4, 没有参数, 捕获了其环境中的num和x两个i32的变量, move表示num和x的所有权转移到闭包中
27 // 闭包4相当于 struct{num: &i32, x: &i32}
28 let c4 = move || num * x;
29 println!("size_of_val(&c4) = {}", size_of_val(&c4)); // size_of_val(&c4) = 8
30}
例3中我们声明了c1, c2, c3, c4四个闭包, 它们是4个不同的匿名类型,它们在内存中的排布与结构体类似,结构体的字段为闭包所捕获的变量:
- 闭包c1没有捕获其环境中任何变量,所以c1就相当于
struct{}
, 打印闭包大小size_of_val(&c1) = 0
- 闭包c2没有捕获其环境中任何变量,所以c2就相当于
struct{}
, 打印闭包大小size_of_val(&c2) = 0
- 闭包c3捕获了num和i32两个变量, 注意这里的捕获方式是以不可变引用形式捕获的, 即闭包内是
&num
和&x
两个不可变引用。所以c3就相当于struct{num: &i32, x: &i32}
在64位机器上各占用8个字节,因此打印闭包大小size_of_val(&c3) = 16
- 闭包c4捕获了num和i32两个变量, 注意这里闭包参数列表前的
move
关键字表示捕获方式是强制以获取所有权的方式捕获,因此闭包内部捕获的是numhex。所以c4就相当于struct{num: i32, x: i32}
,因此打印闭包大小size_of_val(&c4) = 8
从前面对例3代码的分析,能够在编译时确认闭包类型所占内存大小时,闭包将会被存储在栈上。
关于闭包捕获其环境中的变量的捕获方式,后边会介绍
3.Fn、FuMut、FnOnce traits #
当将闭包作为函数的参数或者数据结构(例如结构体)中的某个字段时,需要指定闭包需要满足的约束,需要使用trait bounds告诉调用者闭包的约束。
为此rust标准库中提供了Fn
、FuMut
、FnOnce
三个trait,所有的闭包至少实现了它们中的一个。
注意rust中的函数也实现了这三个trait。
以下是这三个trait的定义:
1// std::ops::Fn
2pub trait Fn<Args>: FnMut<Args> {
3 extern "rust-call" fn call(&self, args: Args) -> Self::Output;
4}
5
6// std::ops::FnMut
7pub trait FnMut<Args>: FnOnce<Args> {
8 extern "rust-call" fn call_mut(
9 &mut self,
10 args: Args
11 ) -> Self::Output;
12}
13
14// std::ops::FnOnce
15pub trait FnOnce<Args> {
16 type Output;
17 extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
18}
从这三个trait的定义上看,Fn继承了FnMut, FnMut继承了FnOnce,既然所有的闭包至少实现了它们中的一个,根据这个继承关系可以推断出所有闭包一定都实现了FnOnce。 从三个trait的继承关系还能看出,任何需要FnOnce或者FnMut的场景,都可以传入满足Fn的闭包。
例4:
1fn main() {
2 let x = 3;
3 let square = || x * x;
4 println!("{}", square());
5
6 foo1(square);
7 foo2(square);
8 foo3(square);
9}
10
11fn foo1<F>(f: F)
12where
13 F: Fn() -> i32,
14{
15 println!("{}", f());
16}
17
18fn foo2<F>(mut f: F)
19where
20 F: FnMut() -> i32,
21{
22 println!("{}", f());
23}
24
25fn foo3<F>(f: F)
26where
27 F: FnOnce() -> i32,
28{
29 println!("{}", f());
30}
上面例1中, trait bound F: Fn() -> i32
、F: FnMut() -> i32
、F: FnOnce() -> i32
都表示F是一个有0个参数,返回i32的闭包。
它们的区别是闭包从环境中捕获变量的方式不同。后边我们会介绍。
4.闭包捕获变量的三种方式 #
闭包可以捕获其环境并访问其被定义的作用域的变量,这是函数所不具备的功能。
当闭包从环境中捕获一个值,闭包会在闭包体中存储这个值以供使用,这就会产生额外的内存开销,这点在使用时需要注意。
闭包可以通过三种方式从环境中捕获值,直接对应函数的三种获取参数的方式: 获取所有权、可变借用、不可变借用。 这三种捕获值的方式被编码为前面提到的三个Fn的trait:
FnOnce
: 闭包以获取所有权的方式捕获其环境中的变量,FnOnce中的Once表示闭包不能多次获取相同变量的所有权,因此它只能被调用一次FnMut
: 闭包以获取可变引用的方式捕获其环境中的变量Fn
: 闭包以获取不可变引用的方式捕获其环境中的变量
由于闭包至少可以被调用一次,因此所有的闭包都实现了FnOnce trait,这和前面从三个trait的继承关系中推断的结论是一致的。。
可以在闭包的参数列表前使用move
关键字,这样将强制闭包以获取所有权的方式捕获其环境中的变量。
例5:
1fn main() {
2 let mut word = String::from("hello");
3
4 // Fn
5 let len1 = || word.len();
6 println!("{}", len1());
7
8 // FnMut
9 let mut len2 = || {
10 word.push_str("world");
11 word.len()
12 };
13 println!("{}", len2());
14
15 println!("{}", word);
16
17 // FnOnce
18 let len3 = move || word.len();
19 println!("{}", len3());
20 // println!("{}", word); borrow of moved value: `word`
21}
5.为闭包实现某个trait #
闭包除了可以作为函数被调用,除了以Fn, FnMut, FnOnce trait bounds约束的前提下作为函数参数、函数返回值、自定义结构的名字段外,还可以为其实现某个trait,使其表现出其它的行为。
例6:
1pub trait Handler {
2 fn handle(&self, request: String);
3}
4
5impl<F> Handler for F
6where
7 F: Fn(String),
8{
9 fn handle(&self, request: String) {
10 (*self)(request)
11 }
12}
13
14fn add_handler(handler: impl Handler) {
15 // ...
16}
17
18fn main() {
19 add_handler(|req| println!("{req}"));
20}