rust 快速入门——13 闭包

164 阅读15分钟

[!|center] 普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

闭包

闭包是可以捕获环境的匿名函数

Rust 的 闭包closures)是可以保存在一个变量中的匿名函数。可以在一个地方创建闭包,然后在不同的上下文中执行闭包。不同于函数,闭包允许捕获(使用)被定义时所在作用域中的数据

闭包在形式上也类似函数,|| 相当于包裹函数参数的括号 ()|| 用来包裹闭包的参数。后面是闭包体,闭包体多条语句需要用大括号 {} 包裹,单条语句可省略 {}。比如:

fn main() {
    use std::mem;
    let color = String::from("green");

    // 创建闭包,捕获环境变量 color,闭包赋值给变量 print
    let print = || println!("`color`: {}", color);

    fn test() {
        // println! ("`color`: {}", color); //函数这样使用 color 变量是非法的!
    }

    // 调用闭包
    print();
}

第 6 行,定义了一个闭包,可以看到,闭包直接使用了环境上下文变量 color 。闭包绑定给一个变量 print ,之后使用变量名 print 和括号来调用闭包,就像变量名是函数名一样。

函数不能使用环境变量,比如第 9 行,在函数中使用 color 变量是非法的。

闭包的本质

闭包本质上是一个具有方法的结构体,结构体封装了捕获的变量。比如上例,闭包捕获的是 String 类型变量 color,则闭包结构体类似于:

struct Closure{
	color:String
}

闭包结构体的关联方法就是闭包体中的语句

相当于

struct Closure {
    color: String, // (ptr|cap|len)=24字节
}
fn main() {
    fn get_closure() -> Closure {
        let color = String::from("green");

        impl Closure {  // 关联一个方法
            fn print(&self) {
                println!("`color`: {}", &self.color);
            }
        }

        Closure { color } // 创建结构体,获取String变量color所有权
    } // color 所有权已经转移到了print变量的结构体中,get_closure函数结束并不需要销毁color

    let closure = get_closure(); // closure获得了Closure结构体
    closure.print();
}

印证:

use std::{collections::HashMap, mem::size_of_val};
fn main() {
    // c1 没有参数,也没捕获任何变量,从代码输出可以看到,c1 长度为 0;
    let c1 = || println!("hello");

    // c2 有一个 i32 作为参数,没有捕获任何变量,长度也为 0,可以看出参数跟闭包的大小无关
    let c2 = |i: i32| println!("hello: {}", i);

    let name = String::from("hello");
    // c3 捕获了一个对变量 name 的引用(println!自动将参数转换为引用),这个引用是 &String,长度为 8。而 c3 的长度也是 8;
    let c3 = || println!("hello: {}", name);

    let name1 = name.clone();
    // c4 捕获了变量 name1,由于用了 move,它们的所有权移动到了 c4 中。
    // c4 长度是 24,等于 String类型长度: (ptr|cap|len)=24字节
    let c4 = move || println!("hello: {}", name1);

    println!(
        "c1: {}, c2: {}, c3: {}, c4: {},  main: {}",
        size_of_val(&c1),
        size_of_val(&c2),
        size_of_val(&c3),
        size_of_val(&c4),
        size_of_val(&main),
    )
}
// 程序输出:c1: 0, c2: 0, c3: 8, c4: 24,  main: 0

闭包类型推断和注解

函数与闭包还有更多区别。闭包并不总是要求像 fn 函数那样在参数和返回值上注明类型。函数中需要类型注解是因为它们是暴露给用户的显式接口的一部分。严格定义这些接口对保证所有人都对函数使用和返回值的类型理解一致是很重要的。与此相比,闭包并不用于这样暴露在外的接口:它们储存在变量中并被使用,不用命名它们或暴露给库的用户调用

闭包通常很短,并只关联于小范围的上下文而非任意情境。在这些有限制的上下文中,编译器能可靠地推断参数和返回值的类型,类似于它是如何能够推断大部分变量的类型一样(同时也有编译器需要闭包类型注解的罕见情况)。

类似于变量,如果我们希望增加明确性和清晰度也可以添加类型标注,坏处是使代码变得更啰嗦。

有了类型注解,闭包的语法就更类似函数了。如下是一个对其参数加一的函数的定义与拥有相同行为闭包语法的对比。这里增加了一些空格来对齐相应部分。这展示了除了使用竖线以及一些可选语法外,闭包语法与函数语法有多么地相似:

fn  add_one_v1   (x: u32) -> u32 { x + 1 } // 函数定义
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

第一行展示了一个函数定义,第二行展示了一个完整标注的闭包定义。第三行闭包定义中省略了类型注解,而第四行去掉了可选的大括号,因为闭包体只有一个表达式。这些都是有效的闭包定义,并在调用时产生相同的行为。

编译器会为闭包定义中的每个参数和返回值推断一个具体类型,因此对同一闭包使用不同类型就会得到类型错误。例如,下例第一次使用 String 值调用 example_closure 时,编译器推断这个闭包中 x 的类型以及返回值的类型是 String。接着这些类型被锁定进闭包 example_closure 中,第二次使用 u32,就会发生冲突:

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

捕获引用或者移动所有权

闭包可以通过三种方式捕获其环境,它们直接对应到函数获取参数的三种方式:不可变借用可变借用获取所有权。闭包会根据语句体中如何使用被捕获的值决定用哪种方式捕获。

不可变引用

在下例定义了一个捕获名为 list 的 vector 的不可变引用的闭包:

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let only_borrows = || println!("From closure: {:?}", list);

    println!("Before calling closure: {:?}", list);
    only_borrows();
    println!("After calling closure: {:?}", list);
}

因为同时可以有多个 list 的不可变引用,所以在闭包定义之前和之后,闭包调用之前和之后,代码仍然可以访问 list

可变引用

使用可变引用

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let mut borrows_mutably = || list.push(7);
	// println!("Before calling closure: {:?}", list); //错误:在闭包定义和调用之间不能有 `list` 的不可变引用
    borrows_mutably();
    println!("After calling closure: {:?}", list);
}

代码可以正常编译和运行。注意在 borrows_mutably 闭包的定义和调用之间不再有 println!,当 borrows_mutably 定义时,它捕获了 list 的可变引用。闭包在被调用后就不再被使用,这时可变借用结束。因为当可变借用存在时不允许有其它的借用,所以在闭包定义和调用之间不能有 list 的不可变引用来进行打印。

注意第 5 行将闭包绑定到可变的变量 borrows_mutably,这是由于闭包本质上是一个具有方法的结构体,结构体封装了捕获的变量,声明 borrows_mutably 可变,意味着可以通过 borrows_mutably 变量修改变量内容,而变量的内容就是捕获的变量 list,对于闭包来说,修改闭包捕获的数据的方式就是调用闭包:borrows_mutably()

获取所有权

如果希望强制闭包获取它用到的环境中值的所有权,可以在参数列表前使用 move 关键字,比如:

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    let get_owner = move || println!("From closure: {:?}", list);
    // println!("Before calling closure: {:?}", list); // 错误!
    get_owner();
    // println!("After calling closure: {:?}", list); // 错误!
}

第 5 行,闭包定义时通过 move 关键字强制捕获了 list 的所有权,因此环境不能再使用它。

与函数和闭包相关的 trait

在 Rust 中,函数和闭包都是实现了 FnFnMutFnOnce 特性(trait)的类型。任何实现了这三种特性其中一种的类型的对象,都是可调用对象,都能像函数和闭包一样通过这样 name() 的形式调用,() 在 Rust 中是一个操作符,操作符在 Rust 中是可以重载的。Rust 的操作符重载是通过实现相应的 trait 来实现,而 () 操作符的相应 trait 就是 FnFnMutFnOnce,所以,任何实现了这三个 trait 中的一种的类型,其实就是重载了 () 操作符。

FnOnce

标准库中的定义:

pub trait FnOnce<Args: Tuple> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

先来理解 FnOnce 的定义:

  1. 关联类型 Output 是 call_once 方法的返回值类型
  2. 方法 call_oncecall_once 第一个参数是 self,它会转移 self 的所有权到 call_once 函数中,生命周期只能是当前作用域,之后就会被释放了。extern "rust-call" 告知编译器采用何种 应用程序二进制接口 ABI (Application Binary Interface)规范去编译这个函数。实现了这个方法,就相当于重载了 () 操作符,实现了 FnOnce 的对象通过 对象名(参数) 即可调用 call_once 方法。这里的 args: Args 就是调用该方法时的参数。Args 是泛型参数,其类型为 TupleTuple 是一个标记 trait,用于告知编译器 Args 是一个元组。Tuple trait 由 Rust 内部使用,用户类型不能去实现这个 trait。

下面的例子定义了一个 MyStruct 结构体,并实现了 FnOnce trait,重载了 () 操作符。声明一个 MyStruct结构体对象 my_struct,达到的效果是 my_struct 可以像函数那样使用 :my_struct()

值得注意的是,Rust 不允许手动实现 FnFnMutFnOnce trait,并且这些 trait 并不是稳定的功能,你必须使用 nightly 版本的 Rust,并且在 main.rs 顶端加上这一行注解#![feature(unboxed_closures, fn_traits)]

#![feature(unboxed_closures, fn_traits)]  
  
struct MyStruct {  
    data: i32,  
}  
  
impl FnOnce<(i32, i32, i32)> for MyStruct {  
    type Output = ();  
    extern "rust-call" fn call_once(self, (x, y, z): (i32, i32, i32)) -> Self::Output {  
        println!("{}", self.data + x + y + z);  
    }  
}  
  
fn main() {  
    let a = 5;  
    let my_struct = MyStruct { data: a };  
    my_struct(1, 2, 3);  
}
// 输出:11

第 7 行,FnOnce<(i32, i32, i32) 指定了泛型参数单例化为 (i32, i32, i32) ,这是一个元组,实际上对应着第 17 行的 my_struct(1, 2, 3) 中的参数。

第 8 行,指定关联类型 Output(),这是一个单元(unit)元组,表示空的返回类型。

第 9 行,定义了 call_once 方法也就重载了 () 操作符。方法的第一个参数 self 代表着待用这个方法的对象;第二个参数 (x, y, z): (i32, i32, i32),通过模式匹配 解构了元组的值,xyz 对应着第 17 行的 my_struct(1, 2, 3) 中的参数。返回值类型为 Self::Output,也就是 (),可以理解为没有返回值。

闭包定义了具体实现代码,编译器根据代码如何使用捕获的变量,默认会自动给闭包添加 FnFnMutFnOnce trait 三者之一,进而程序在其它地方使用闭包时,编译器就能够根据闭包实现的 trait,判断使用方式是否合法。

看一个编译器自动为闭包添加了 FnOnce trait 的例子:

#[derive(Debug)]
struct MyStruct {
    data: String,
}

impl Drop for MyStruct {
    fn drop(&mut self) {
        println!("destroyed struct MyStruct");
    }
}

fn fn_once<F>(func: F)
where
    F: FnOnce()
{
    println!("fn_once begins");
    func();
    println!("fn_once ended");
}

fn main() {
    let my_struct = MyStruct { data: "fn_once".to_string() };
    let closure = move || println!("FnOnce closure calls: {:?}", my_struct);
    fn_once(closure);
    println!("main ended");
}

注意第 12 -14 行这种奇怪的写法,ruat 官方资料并未给出明确的解释。第 13 行是对泛型 F 的 trait 约束,按理应写为:

fn fn_once<F>(func: F) 
	where F: FnOnce 
{   ...   }

但是 rust 各种文档给出的示例都要求写为:where F: FnOnce(参数)->返回值,其中 FnOnce (参数)->返回值 显然从形式上不像一个 trait,而更像是对闭包的类型声明。

打印结果如下:

fn_once begins
FnOnce closure calls: MyStruct { data: "fn_once" }
destroyed struct MyStruct
fn_once ended
main ended

但是如果闭包运行两次,比如:

fn fn_once<F>(func: F)
where
    F: FnOnce()
{
    println!("fn_once begins");
    func();
    func();
    println!("fn_once ended");
}

则编译器就报错了:

error[E0382]: use of moved value: `func`
  --> src/main.rs:18:5
   |
12 | fn fn_once<F>(func: F)
   |               ---- move occurs because `func` has type `F`, which does not implement the `Copy` trait
...
17 |     func();
   |     ------ `func` moved due to this call
18 |     func();
   |     ^^^^ value used here after move
   |
note: `FnOnce` closures can only be called once
  --> src/main.rs:14:8
   |
14 |     F: FnOnce()
   |        ^^^^^^^^ `F` is made to be an `FnOnce` closure here
...
17 |     func();
   |     ------ this value implements `FnOnce`, which causes it to be moved when called

这是为什么呢?还是回到 FnOnce 的定义,参数类型是 self,所以在 func 第一次执行完之后,之前捕获的变量已经被释放了,所以已经无法在执行第二次了。所以,如果要运行多次,可以使用 FnMutFn

再看一个示例:

fn main() {  
    let name = String::from("peter");  
    // 这个闭包啥也不干,只是把捕获的参数返回去  
    let closure = move |greeting: String| (greeting, name);  
    // 闭包内的name发生了转移,所以这里闭包变得不完整,无法再次使用  
    let result = closure("hello".to_string());  
  
    println!("result: {:?}", result);  
  
    // 无法再次调用  
    // let result = closure("hi".to_string());  
}
  1. 如果一个闭包并不获取捕获的数据的所有权,那么它就不是 FnOnce 的。
  2. 一旦它被当做 FnOnce 调用,捕获的变量的所有权就会被转移到 call_once 函数的作用域中,之后就无法再次调用。

FnMut

标准库中的定义:

pub trait FnMut<Args: Tuple>: FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
  1. FnMut “继承”了 FnOnce,或者说 FnOnceFnMut 的 super trait
  2. FnMut 也拥有 Output 这个关联类型和 call_once 这个方法
  3. 还有一个 call_mut 方法。注意 call_mut 传入 &mut self,是可变借用,它不移动 self 所有权,不会释放传入的变量,所以 FnMut 可以被多次调用
  4. FnOnceFnMut 的 super trait,所以,一个 FnMut 闭包,可以被传给一个需要 FnOnce 的上下文,此时调用闭包相当于调用了call_once

将之前的 FnOnce 类型的闭包的例子修改为 FnMut 类型:

#[derive(Debug)]
struct MyStruct {
    data: String,
}

impl Drop for MyStruct {
    fn drop(&mut self) {
        println!("destroyed struct MyStruct");
    }
}

fn fn_mut<F>(mut func: F)
where
    F: FnMut(),
{
    println!("fn_mut begins");
    func();
    func();   //可以多次调用
    println!("fn_mut ended");
}

fn main() {
    let mut my_struct = MyStruct { data: "fn_once".to_string() };
    let closure = || {
        println!("FnMut closure calls: {:?}", my_struct);
        my_struct.data = "fn_mut".to_string();  // 修改数据
    };
    fn_mut(closure);
    println!("main ended");
}

可以看出 FnMut 类型的闭包是可以运行多次的,且可以修改捕获变量的值。

FnOnceFnMut 的 super trait,所以,一个 FnMut 闭包,可以被传给一个需要 FnOnce 的上下文,此时调用闭包相当于调用了 call_once,将例子第 12-20 行改为,程序正确运行:

fn fn_mut<F>(mut func: F)  
where  
    F: FnOnce(),  
{  
    println!("fn_once begins");  
    func();  
    println!("fn_once ended");  
}

Fn

标准库中的定义:

pub trait Fn<Args: Tuple>: FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
  1. 它“继承”了 FnMut,或者说 FnMutFn 的 super trait
  2. 任何需要 FnOnce 或者 FnMut 的场合,都可以传入满足 Fn 的闭包
  3. 参数类型是 &self,所以,这种类型的闭包是不可变借用,不会改变变量,也不会释放该变量。所以可以运行多次。

将之前的 FnOnce 类型的闭包的例子修改为 Fn 类型:

#[derive(Debug)]
struct MyStruct {
    data: String,
}

impl Drop for MyStruct {
    fn drop(&mut self) {
        println!("destroyed struct MyStruct");
    }
}

fn fn_immut<F>(func: F)
where
    F: Fn(),
{
    println!("fn begins");
    func();
    func(); // 可以多次调用
    println!("fn ended");
}

fn main() {
    let my_struct = MyStruct { data: "fn".to_string() };
    let closure = || {
        println!("FnMut closure calls: {:?}", my_struct);
        // my_struct.data = "fn_mut".to_string();  // 不能修改数据!
    };
    fn_immut(closure);
    println!("main ended");
}

可以看出 Fn 类型的闭包是可以运行多次的,但不可以修改捕获变量的值。

常见的错误

有时候在使用 Fn/FnMut 这里类型的闭包,编译器经常会给出这样的错误:

# ...
cannot move out of 'xxx', a captured variable in an Fn(FnMut) closure
# ...

复现这种情形:

fn main() {
    fn fn_immut<F>(f: F)
    where
        F: Fn() -> String,
    {
        println!("calling Fn closure from fn, {}", f());
    }

    let str = "Fn".to_string();
    fn_immut(|| str); // 闭包返回一个字符串
}

因为闭包最后返回了 str,交还了所有权,是不能再运行第二次了,闭包不再是 str 的所有者,因此编译器推导出这个闭包是 FnOnce 类型的。

Fn/FnMut 是被认定可以多次运行的,不能将 FnOnce 类型的闭包作为参数传给要求 Fn/FnMut 类型参数的函数。

修复方法,第 10 行改为:

fn_immut(|| str.clone());

闭包使用场景

  1. 多线程
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {:?}", list);

    thread::spawn(move || println!("From thread: {:?}", list))
        .join()
        .unwrap();
}

关于 Rust 线程在后面的章节中介绍。

  1. 闭包作为函数参数

标准库 core::iter::traits::iterator trait 中的大部分方法都可以接受闭包作为参数,比如 map:

fn main() {  
    let v = vec![1, 2, 3];  
    let v_squared: Vec<i32> = v.iter().map(|x| x * x).collect();  
    println!("{:?}", v_squared);  
}
  1. 闭包作为返回值
fn main() {  
    fn get_closure() -> impl FnOnce() {  
        let color = String::from("green");  
        let closure = move || println!("`color`: {}", color);  
        return closure;  
    } // color 离开了get_closure函数作用域,但是被闭包使用了,因此不能被销毁  
  
    let closure = get_closure(); //将返回的闭包赋值给x  
    closure(); 
}