笔记 - 闭包

203 阅读7分钟

闭包

闭包(Closure):通常是指词法闭包,是一个持有外部环境变量的函数。

外部环境:是指闭包定义时所在的词法作用域。

自由变量:外部环境变量,是指并不是在闭包内定义的变量。

将自由变量和自身绑定的函数就是闭包

返回闭包样例

fn counter(i: i32) -> impl Fn(i32) -> i32 {
    move |n: i32| n + i  // 或 return move |n: i32| n + i;
}

fn main() {
    let f = counter(5);  // 类型为 impl Fn(i32) -> i32
    println!("f(3) = {}", f(3));
}

这里的闭包类型是 Fn(i32) -> i32,Fn 不是函数指针类型 fn(i32) -> i32,它是一个trait。

闭包的两个特性:

  • 延迟执行。返回的闭包只有在需要调用的时候才会执行。
  • 捕获环境变量。闭包会获取其定义时所在作用域中的自由变量,以供之后调用时使用。

闭包的基本语法

闭包的定义

闭包: 由 管道符(两个对称的竖线) 和 花括号(或圆括号)组合而成。

fn main() {
    let add = |a: i32, b: i32| -> i32 { a + b };
    println!("add(5, 6) = {}", add(5, 6)); // add(5, 6) = 11
}

情形1:管道符里是闭包函数的参数,类型可以省略(若可以被推断)。

fn main() {
    let add = |a, b| -> i32 { a + b };  
    println!("add(5, 6) = {}", add(5, 6)); // 整数默认是 i32,所以没问题。
    // println!("add(5.1, 6.2) = {}", add(5.1, 6.2));  // 这样就不行,或报错类型不匹配。
}

情形2:花括号里面包含的是闭包函数执行体,花括号和返回值也可以省略(若多条语句,不可省略)。

fn main() {
    let add = |a, b| a + b;
    println!("add(5, 6) = {}", add(5, 6));
}

情形3:当闭包没有参数,只有捕获的自由变量时,管道符里的参数也可以省略。

fn main() {
    let (a, b) = (3, 6);
    let add = || a + b;  // 相当于 let add = || 9;
    println!("add() = {}", add());
}

闭包参数类型

闭包的参数可以为任意类型。

fn val() -> i32 {
    3
}

fn main() {
    let add = |a: fn() -> i32, (b, c): (i32, i32)| a() + b + c;
    let r = add(val, (3, 4));
    println!("r = {}", r); // r = 10
}

闭包的类型

经实践发现,两个定义一模一样的闭包属于同一种类型。

fn main() {
    let a = |a: i32| a;
    println!("type of a = {}", type_name(&a));

    let b = |b: i32| b;
    println!("type of b = {}", type_name(&b));

    let c = |a: i32, b: i32| a + b;
    println!("type of c = {}", type_name(&c));

    let v1 = [a, b];
    println!("v1 = {:?}", v1);

    // let v2 = [a, b, c]; // build error, c 的类型与 a, b 不同
}

fn type_name<T>(_: &T) -> &str {
    std::any::type_name::<T>()
}

闭包实现

在 Rust 中,闭包是一种语法糖。闭包和普通函数的差别就是闭包可以捕获环境变中的自由变量。

闭包的三个 trait, Fn、FnMut、FnOnce。

  • FnOnce 调用参数为 self,意味着它会转移方法接受者的所有权。这种方法调用只能被调用一次。

  • FnMut 调用参数为 &mut self,意味着它会对方法接收者进行可变借用。

  • Fn 调用参数为 &self,意味着它会对方法接收者进行不可变借用。这种方法调用可以被调用多次。

闭包与所有权

闭包表达式会由编译器自动翻译为结构体实例,并为其实现 Fn、FnMut、FnOnce 三个 trait 中的一个。

编译时究竟实现的哪一个呢?与所有权相关。

  • FnOnce,表示闭包通过转移所有权来捕获环境中的自由变量,同时意味着该闭包有改变环境的能力。只能调用一次,因为该闭包会消耗自身,对应 self。

  • FnMut,表示闭包以可变借用的方式来捕获环境中的自由变量,同时意味着该闭包有改变环境的能力。也可以多次调用,对应 &mut self。

  • Fn,表示闭包以不可变借用的方式来捕获环境中的自由变量,同时也表示该闭包没有改变环境的能力,并且可以多次调用。对应 &self。

闭包会根据环境变量的类型来决定实现哪种 trait。三种 trait 间的 继承关系如下。

- pub trait FnOnce<Args>
- pub trait FnMut<Args>: FnOnce<Args>
- pub trait Fn<Args>: FnMut<Args>

复制语义类型自动实现 Fn

fn main() {
    let s = "hello";
    let c = || {
        println!("call c, s = {}", s);
    };

    c();
    c();

    println!("s = {}", s);
}

s 为字符串字面量,为复制语义类型。闭包 c 会按照不可变引用来捕获 s。

综上,闭包 c 默认自动实现了 Fn 这个 trait,并且该闭包以不可变借用捕获环境中的自由变量。

移动语义类型自动实现 FnOnce

fn main() {
    let s = "hello".to_string();
    let c = || s;

    let a = c();
    println!("a = {}", a);
    // c(); // error[E0382]: use of moved value: `c`
    // println!("s = {}", s); // error[E0382]: borrow of moved value: `s`
}

变量绑定 s 为 String 类型,是移动语义。因此 闭包会按照 FnOnce 来实现。

使用 move 关键字自动实现 Fn

Rust 针对闭包提供了一个关键字 move,使用此关键字的作用是强制让闭包所定义环境中的自由变量转移到闭包中。

当环境变量为 复制语义 类型时使用 move 关键字的样例:

尽管 move 关键字强制执行,但闭包捕获的 s 执行的对象是 复制语义后获取的心变了。原始的 s 并未失去所有权,此时,该闭包实现的一定是 Fn。**

fn main() {
    let s = "hello";
    let c = move || println!("s = {:?}", s);
    c();   // s = "hello"
    c();   // s = "hello"
    println!("s = {:?}", s);  // s = "hello"
}

当环境变量为 移动语义 类型时使用 move 关键字的样例:

变量绑定 s 为移动语义类型 String,变量已经被转移,经测试 可多次调用闭包c,此时实现该闭包的一定是 Fn。

fn main() {
    let s = "hello".to_string();
    let c = move || println!("s = {:?}", s);
    c();
    c();
    // println!("s = {:?}", s);    // error[E0382]: borrow of moved value: `s`
}

move关键字是否影响闭包本身

fn callFn<F: FnOnce()>(f: F) {
    f()
}

fn main() {
    let mut x = 0;
    let incr_x = || {
        x += 1;
        println!("x = {}", x);
    };
    callFn(incr_x);
    // callFn(incr_x);  // value used here after move

    // 对 复制语义 的变量类型,使用 move 关键字
    let mut y = 0;
    let incr_y = move || {
        y += 1;
        println!("y = {}", y);
    };
    callFn(incr_y);
    callFn(incr_y);

    // 对 移动语义 的变量类型使用 move
    let mut z = vec![];
    let expend_z = move || {
        z.push(16);
        println!("z = {:?}", z);
    };
    callFn(expend_z);
    // callFn(expend_z);  // error[E0382]: use of moved value: `expend_x`
}

  • callFn 使用 FnOnce 作为类型参数,是因为 FnMut 和 Fn 都得 实现 FnOnce trait。
  • 对于闭包 incr_x 由于并未使用 move 关键字,且闭包里面对捕获的变量进行了修改,生成了 FnMut 闭包,因此 x 被转移。
  • FnMut 的闭包在使用 move 关键字时,如果捕获变量是 复制语义类型的,则闭包会自动实现 Copy/Clone; 如果捕获的变量是 移动语义类型的,则闭包不会自动实现 Copy/Clone(出于内存安全的考虑)。

修改环境变量以自动实现 FnMut

...

综合以上的情况,可得出如下的规则

  • 如果闭包中没有捕获任何环境变量,则默认自动实现 Fn。

  • 如果闭包中捕获了复制语义类型的环境变量,则:

    • 如果不需要修改环境变量,无论是否使用 move 关键字,均会自动实现 Fn。

    • 如果需要修改环境变量,则自动实现 FnMut。

  • 如果闭包中捕获了移动语义类型的环境变量,则:

    • 如果不需要修改环境变量,且没有使用 move 关键字,则自动实现 FnOnce。

    • 如果不需要修改环境变量,且使用了 move 关键字,则自动实现 FnOnce。

    • 如果需要修改环境变量,则自动实现 FnMut。

  • 对于 FnMut 的闭包使用 move 关键字,如果捕获的变量是复制语义类型的,则闭包会自动实现 Copy/Clone,否则不会自动实现 Copy/Clone。

闭包作为函数参数和返回值

// to be continued