本文部分内容参考:https://rustyyato.github.io/rust/syntactic/sugar/2019/01/17/Closures-Magic-Functions.html

Rust 语言的函数具有明确的语义,方便静态检查器对其进行编译期检查,Rust 也支持函数指针,类似于 C。但一般情况,Rust 都使用函数项对函数指针进行优化,这样可以做零大小的类型优化。

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 个字节的内存空间。

闭包

函数虽然够用,但是它不能捕获环境变量,而闭包可以。

rust
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: 13

Rust 语言的闭包,没有任何特性,其实是编译期的语法糖实现。|| {} 是闭包的基本语法,|| 为参数列表,后边是闭包体,如果只有一行函数体,{} 可以省略。

闭包是如何通过语法糖实现的

Rust 语言是具有高度一致性的,闭包的实现和所有权保持了高度一致性,Rust 的闭包完全是 Rust 的语法糖实现,其底层实现是基于 struct 和 trait,事实就是三个 trait:

  1. FnOnce —> 所有权转移,只能调用一次
  2. FnMut —> 可变借用 (&mut T),可以对捕获的值做修改
  3. Fn —> 不可变借用,只访问捕获的值

源码定义:

rust
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 。这个需要结合例子来看,先从最简单的例子看起:

rust
let f1 = || println!("hi!");
f1();
f1();

上述代码,f1 闭包没有不过任何外部变量,如果去掉语法糖,大致的原始代码如下:

rust
#[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 也需要实现。最后把

rust
let f1 = || println!("hi!");
f1();
f1();

转换为 Fn 的调用:

rust
let f1 = __closure_0__ {};
Fn::call(&f1, ());
Fn::call(&f1, ());

Fn 和 FnMut 都是可以多次调用的。


接下来,实现一个捕获了外部变量的闭包:

rust
fn main() {
    let s = String::from("hi");
    let f = || println!("{:?}", s);
    f();
}

去掉语法糖,原始代码如下:

rust
#[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 会做何实现呢?

rust
fn main() {
    let s = String::from("hi");
    let f = move || println!("{:?}", s);
    f();
    f();
}

去掉语法糖,原始实现如下:

rust
#[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 不仅会捕获外部变量的所有权,而且会消费捕获变量的所有权,所以只能调用一次。

再来看一个更抽象的情况,将一个引用变量的所有权转移到闭包里:

rust
fn main() {
    let s = String::from("hi");
    let ps = &s;
    let f = move || println!("{:?}", ps);
    f();
    f();
}

去掉语法糖,原始实现:

rust
#[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 的情况,现在我们实现一个闭包,对外部变量进行修改:

rust
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);

去掉语法糖:

rust
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 。


最后我们来看看,如果闭包捕获一个所有权,并把捕获变量消费了呢?

rust
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 语言的设计一致性;我们来看看去掉语法糖的代码:

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 根据规则推断出需要移入所有权。还有些代码的所有权转移会更加隐蔽:

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,但是:

rust
let a = a.into_iter().map(|x| x * 3 + 1);

这一行代码,会把 a 转换为一个迭代器,迭代器是需要消费 a 的。迭代器的实现:

rust
fn into_iter(self) -> Self::IntoIter

第一个参数是 self ,是所有权本身,而不是引用。

逃逸闭包(escaping closures)和非逃逸闭包 (non-escaping closures)

这主要是两个概念,很多编程语言中都有这两个概念,逃逸即指,是否超出作用域:

  • 逃逸闭包:闭包在超出定义它的作用域的地方被调用;常见为函数返回一个闭包
  • 非逃逸闭包:指不能在定义它的作用域之外被调用的闭包。这意味着非逃逸闭包只能在定义它的作用域内部使用,无法被保存或传递到其他函数中。

非逃逸闭包例子:

rust
fn scope_fn() {
    let f = |x:i32| x+1;
    // 在 scope_fn 中执行了闭包 f
    let r = f(2);

    // 作用域结束,闭包销毁
}

逃逸闭包的例子:

rust
fn scope_fn() -> impl FnMut(i32) -> i32 {
    let a = 10;
    move |i| a + i
}

move |i| a + i 闭包定义在函数内部,但是被函数作为返回值扔了出去,并没有在函数体内被调用。所以称为逃逸闭包。

(全文完)