Appearance
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 主要是通过分析外部变量在闭包中的使用方式,通过一系列的规则自动推导出来的。 主要规则如下:
- 如果一个外部变量在闭包中,只通过借用指针
&
使用,那么这个变量就可通过引用&
的方式捕获; - 如果一个外部变量在闭包中,通过
&mut
指针使用过,那么这个变量就需要使用&mut
的方式捕获; - 如果一个外部变量在闭包中,通过所有权转移的方式使用过,那么这个变量就需要使用 “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 这个局部变量,就是这个类型的实例。对我们来说,这个类型是匿名的。