今天来学习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标准库中提供了FnFuMutFnOnce三个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() -> i32F: FnMut() -> i32F: 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}

参考