本文部分内容参考:https://rustyyato.github.io/rust/syntactic/sugar/2019/01/17/Closures-Magic-Functions.html
Rust 语言的函数具有明确的语义,方便静态检查器对其进行编译期检查,Rust 也支持函数指针,类似于 C。但一般情况,Rust 都使用函数项对函数指针进行优化,这样可以做零大小的类型优化。
type Bar = (i32, i32);
fn trans(c: &str) -> Bar {
(2, 3)
}
fn show(f: fn(&str) -> Bar) {
println!("{:?}", f("test"));
}
fn main() {
let a = trans; // a 为函数项
show(a);
let t_a: fn(&str) -> Bar = a; // 显式转为函数指针
show(t_a);
// 测量大小
println!("a size: {:?}", std::mem::size_of_val(&a)); // 0
println!("t_a size: {:?}", std::mem::size_of_val(&t_a)); // 8
}上述代码可以看到,函数项 a 占用的是 0 大小内存空间,而如果显式转为函数指针,则需要占用 8 个字节的内存空间。
闭包
函数虽然够用,但是它不能捕获环境变量,而闭包可以。
fn main() {
// 未捕获环境变量
let f1 = || println!("hi!");
f1();
// 修改了外部变量
let mut arr = [1, 2, 3];
let mut f2 = |i| {
arr[0] = i;
};
f2(12);
println!("arr: {:?}", arr);
// 访问了外部变量
let an = 13;
let f3 = || println!("an is: {:?}", an);
f3();
}
// 输出:
hi!
arr: [12, 2, 3]
an is: 13Rust 语言的闭包,没有任何特性,其实是编译期的语法糖实现。|| {} 是闭包的基本语法,|| 为参数列表,后边是闭包体,如果只有一行函数体,{} 可以省略。
闭包是如何通过语法糖实现的
Rust 语言是具有高度一致性的,闭包的实现和所有权保持了高度一致性,Rust 的闭包完全是 Rust 的语法糖实现,其底层实现是基于 struct 和 trait,事实就是三个 trait:
- FnOnce —> 所有权转移,只能调用一次
- FnMut —> 可变借用 (&mut T),可以对捕获的值做修改
- Fn —> 不可变借用,只访问捕获的值
源码定义:
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
// FnMut 对 FnOnce 做了“继承”
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
// Fn 对 FnMut 做了“继承”
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}这里说“继承”,意思是实现 FnMut trait 就必须先实现 FnOnce ,实现 Fn trait 就必须先实现FnMut 和FnOnce 。这是 Rust Trait 的特性。
当定义一个闭包的时候,Rust 检查器,会优先尝试实现一个 Fn Trait,然后是 FnMut,最后是 FnOnce 。这个需要结合例子来看,先从最简单的例子看起:
let f1 = || println!("hi!");
f1();
f1();上述代码,f1 闭包没有不过任何外部变量,如果去掉语法糖,大致的原始代码如下:
#[derive(Clone, Copy)]
struct __closure_0__ {}
impl FnOnce<()> for __closure_0__ {
type Output = ();
fn call_once(self, args: ()) -> () {
println!("hi!");
}
}
impl FnMut<()> for __closure_0__ {
fn call_mut(&mut self, args: ()) -> () {
println!("hi!");
}
}
impl Fn<()> for __closure_0__ {
fn call(&self, (): ()) -> () {
println!("hi!");
}
}
let f1 = __closure_0__ {};
Fn::call(&f1, ());
Fn::call(&f1, ());Rust 先定义一个 __closure_0__ 结构体,由于闭包没有捕获任何外部变量,所以结构体不需要任何元素。然后 Rust 按照 Fn → FnMut → FnOnce 的优先级,实现对应的 Trait,由于我们没有对任何变量进行所有权转移,也没有任何外部的可变引用。所以可以实现 Fn Trait,由于 Fn ”继承” 了 FnMut 和 FnOnce ,所以这两个 Trait 也需要实现。最后把
let f1 = || println!("hi!");
f1();
f1();转换为 Fn 的调用:
let f1 = __closure_0__ {};
Fn::call(&f1, ());
Fn::call(&f1, ());Fn 和 FnMut 都是可以多次调用的。
接下来,实现一个捕获了外部变量的闭包:
fn main() {
let s = String::from("hi");
let f = || println!("{:?}", s);
f();
}去掉语法糖,原始代码如下:
#[derive(Clone, Copy)]
struct __closure_1__<'a> { // 'a 是生命周期参数
s: &'a String, // 注意,这里是 &String, 不是 String
}
// FnOnce FnMut ... 省略实现部分
impl<'a> Fn<()> for __closure_1__<'a> {
// type Output = ();
fn call(&self, (): ()) -> () {
println!("{}", *self.s);
}
}
let s = String::from("hi");
let f = __closure_1__ { s: &s };
Fn::call(&f, ());由于闭包体中只访问了 s ,所以实际捕获的是 s 的引用:&s ,然后把引用存入到闭包结构体中。最终闭包实际上为 Fn Trait 类型。
如果我们在闭包前加上 move 关键字,将 s 的所有权移入闭包,rust 会做何实现呢?
fn main() {
let s = String::from("hi");
let f = move || println!("{:?}", s);
f();
f();
}去掉语法糖,原始实现如下:
#[derive(Clone)]
struct __closure_1__ {
s: String, // 注意,这次是 String, 不是 &String
}
// FnOnce FnMut ... 省略实现部分
impl<'a> Fn<()> for __closure_1__<'a> {
// type Output = ();
fn call(&self, (): ()) -> () {
println!("{}", self.s);
}
}
let s = String::from("hi");
let f = __closure_1__ { s: s }; // 这里直接将 s 所有权转移给结构体内部了
Fn::call(&f, ());
Fn::call(&f, ());move 关键字只是将 s 的所有权转移进了闭包结构体,由于闭包的实现中,依然只是访问 s 的值。所以 Fn Trait 依然能够符合规则,所以依旧实现的是 Fn Trait。记住,Rust 总会按照优先级:
Fn → FnMut → FnOnce
的顺序进行实现,因为:
- Fn 是最松的规则,即不可变借用(可以多次调用),这里的不可变是指闭包结构体本身,而不是捕获的值。
- FnMut 是独占借用,但是依然可以多次调用。
- FnOnce 是转移所有权,理论上只能调用一次,因为 FnOnce 不仅会捕获外部变量的所有权,而且会消费捕获变量的所有权,所以只能调用一次。
再来看一个更抽象的情况,将一个引用变量的所有权转移到闭包里:
fn main() {
let s = String::from("hi");
let ps = &s;
let f = move || println!("{:?}", ps);
f();
f();
}去掉语法糖,原始实现:
#[derive(Clone, Copy)]
struct __closure_1__<'a> {
ps: &'a String
}
// FnOnce FnMut ... 省略实现部分
impl<'a> Fn<()> for __closure_1__<'a> {
// type Output = ();
fn call(&self, (): ()) -> () {
println!("{}", self.ps);
}
}
let s = String::from("hi");
let ps = &s;
let f = __closure_1__ { ps: ps };
Fn::call(&f, ());
Fn::call(&f, ());由于捕获的本身就是一个引用,所以闭包结构体内部,也是&String类型。唯一需要注意的是,引用是有生命周期的,所以用 ‘a 来指明引用的生命周期,关于生命周期不是本节的重点,这里先不做讨论。
接下来我们来看 FnMut 的情况,现在我们实现一个闭包,对外部变量进行修改:
let mut counter: u32 = 0;
let delta: u32 = 2;
let mut next = || { // 注意 next 闭包本身也需要 mut 修饰
counter += delta;
counter
};
assert_eq!(next(), 2);
assert_eq!(next(), 4);
assert_eq!(next(), 6);去掉语法糖:
struct __closure_4__<'a, 'b> {
counter: &'a mut u32,
delta: &'b u32
}
// ...省略 FnOnce 的实现...
impl<'a, 'b> FnMut<()> for __closure_4__<'a, 'b> {
// type Output = u32;
fn call_mut(&mut self, (): ()) -> u32 {
*self.counter += *self.delta;
*self.counter
}
}
let mut counter: u32 = 0;
let delta: u32 = 2;
let mut next = __closure_4__ {
counter: &mut counter,
delta: &delta
};
assert_eq!(FnMut::call_mut(&mut next, ()), 2);
assert_eq!(FnMut::call_mut(&mut next, ()), 4);
assert_eq!(FnMut::call_mut(&mut next, ()), 6);这次闭包修改了外部变量 counter,所以需要捕获一个 counter 的可变借用,对 delta 只是访问,所以值需要一个不可变借用,另外 delta 本身也是不可变的值。
相应的,由于闭包结构体自身持有一个 &mut counter ,所以自己也得是可变的,所以需要实现 FnMut ,Fn 已经无法满足规则(Fn 是自身不可变借用)。FnMut 依然可以多次调用。
通过去语法糖,看原始的实现,也就明白了为什么 next 本身也要声明为 mut 。因为闭包本身是结构体,结构体内部的 counter 是可变的借用,所以 next 也必须声明为可变的。不然无法调用 call_mut 。
最后我们来看看,如果闭包捕获一个所有权,并把捕获变量消费了呢?
fn main() {
let s = String::from("hi");
let f = || s;
let rs1 = f();
let rs2 = f(); // 编译报错,❌,s 已经被第一次调用消费掉了。
println!("s is: {:?}", s); // 编译报错,❌,s 所有权已经被转移
}我一直在代码示例中,调两次闭包,目的就是验证是否是 FnOnce 。显然上述闭包代码实现的就是 FnOnce Trait。let f = || s; 这个闭包很简单,捕获外部变量 s,并将它返回。这样最初的 s 所指向的堆内存 “hi” 的所有权已经被转移给 rs1 ,所以闭包不能再次被使用了,同样 s 变量也不能被使用了,这样体现了 Rust 语言的设计一致性;我们来看看去掉语法糖的代码:
#[derive(Clone)]
struct __closure_5__ {
a: String
}
impl FnOnce<()> for __closure_5__ {
type Output = String;
fn call_once(self, (): ()) -> String {
self.a // 所有权转移出去
}
}
let s = String::from("hi");
let f = __closure_5__ { s: s }; // 转移所有权到闭包结构体内部
let rs1 = FnOnce::call_once(f, ()) // 注意这里也是 f 本身的所有权,不是 &
// let rs1 = FnOnce::call_once(f, ()),错误代码,f 的所有权已经被转移
// println!("s is: {:?}", s); 错误代码,s 的所有权已经被转移原始代码并没有显式的调用 move 来移交所有权,但是 Rust 根据规则推断出需要移入所有权。还有些代码的所有权转移会更加隐蔽:
fn main() {
let a = vec![0, 1, 2, 3, 4, 5, 100];
// 注意, 没有 `move`
let transform = || {
let a = a.into_iter().map(|x| x * 3 + 1);
a.sum::<u32>()
};
println!("{}", transform());
println!("s is: {:?}", a); // 编译报错 ❌, a 所有权已经被转移
}虽然没有明确的消费 a,但是:
let a = a.into_iter().map(|x| x * 3 + 1);这一行代码,会把 a 转换为一个迭代器,迭代器是需要消费 a 的。迭代器的实现:
fn into_iter(self) -> Self::IntoIter第一个参数是 self ,是所有权本身,而不是引用。
逃逸闭包(escaping closures)和非逃逸闭包 (non-escaping closures)
这主要是两个概念,很多编程语言中都有这两个概念,逃逸即指,是否超出作用域:
- 逃逸闭包:闭包在超出定义它的作用域的地方被调用;常见为函数返回一个闭包
- 非逃逸闭包:指不能在定义它的作用域之外被调用的闭包。这意味着非逃逸闭包只能在定义它的作用域内部使用,无法被保存或传递到其他函数中。
非逃逸闭包例子:
fn scope_fn() {
let f = |x:i32| x+1;
// 在 scope_fn 中执行了闭包 f
let r = f(2);
// 作用域结束,闭包销毁
}逃逸闭包的例子:
fn scope_fn() -> impl FnMut(i32) -> i32 {
let a = 10;
move |i| a + i
}move |i| a + i 闭包定义在函数内部,但是被函数作为返回值扔了出去,并没有在函数体内被调用。所以称为逃逸闭包。
(全文完)