Skip to content

22.1 变量捕获

接下来我们研究一下 closure 的原理。Rust 目前的 closure 实现,又叫作 unboxed closure,它的原理与 C++11 的 lambda 非常相似。当一个 closure 创建的时候,编译器帮我们生成了一个匿名 struct 类型,通过自动分析 closure 的内部逻辑,来决定该结构体包括哪些数据,以及这些数据该如何初始化。

考虑以下例子:

rust
fn main() {
    let x = 1_i32;
    let add_x = | a | x + a;
    let result = add_x(5);
    println!("result is {}", result);
}

我们来思考一下:如果不使用闭包来实现以上逻辑,该怎么做?方法如下:

rust
struct Closure {
    inner1: i32
}

impl Closure {
    fn call(&self, a: i32) -> i32 {
        self.inner1 + a
    }
}

fn main() {
    let x = 1_i32;
    let add_x = Closure{ inner1: x};
    let result = add_x.call(5);
    println!("result is {}", result);
}

上面这个例子,我们模拟了一个闭包的原理,实际上 Rust 编译器就是用类似的手法来处理闭包语法的。对比一下使用闭包语法的版本和手动实现的版本,我们可以看到,创建闭包的时候,就相当于创建了一个结构体,我们把需要捕获的环境变量存到这个结构体中。闭包调用的时候,相当于调用了跟这个结构体相关的一个成员函数。

但是,还有几个问题没有解决。当编译器把闭包语法糖转换为普通的类型和函数调用的时候:

(1)结构体内部的成员应该用什么类型,如何初始化?应该用i32或是&i32还是&mut i32

(2)函数调用的时候self应该用什么类型?应该写self或是&self还是&mut self

理解了这两个问题的答案,就能完全理解了 Rust 的闭包的原理。

关于第一个问题,Rust 主要是通过分析外部变量在闭包中的使用方式,通过一系列的规则自动推导出来的。 主要规则如下:

  1. 如果一个外部变量在闭包中,只通过借用指针 & 使用,那么这个变量就可通过引用 & 的方式捕获;
  2. 如果一个外部变量在闭包中,通过 &mut 指针使用过,那么这个变量就需要使用 &mut 的方式捕获;
  3. 如果一个外部变量在闭包中,通过所有权转移的方式使用过,那么这个变量就需要使用 “by value” self 的方式捕获。

简单点总结规则是,在保证能编译通过的情况下,编译器会自动选择一种对外部影响最小的类型存储。 对于被捕获的类型为 T 的外部变量,在匿名结构体中的存储方式选择为:尽可能先选择 &T 类型,其次选择 &mut T 类型,最后选择 T 类型。示例如下:

rust
struct T(i32);

fn by_value(_: T) {}
fn by_mut(_: &mut T) {}
fn by_ref(_: &T) {}

fn main() {
    let x: T = T(1);
    let y: T = T(2);
    let mut z: T = T(3);

    let closure = || {
        by_value(x);
        by_ref(&y);
        by_mut(&mut z);
    };

    closure();
}

以上闭包捕获了外部的三个变量x y z。 其中,y 通过 &T 的方式被使用了; z 通过 &mut T的方式被使用了; x 通过 T 的方式被使用了。 编译器会根据这些信息,自动生成结构类似下面这样的匿名结构体:

rust
// 实际类型名字是编译器按某些规则自动生成的
struct ClosureEnvironment<'y, 'z> {
    x: T,
    y: &'y T,
    z: &'z mut T,
}

而原示例中的 closure 这个局部变量,就是这个类型的实例。对我们来说,这个类型是匿名的。

Released under the MIT License