Rust 中的闭包

1,085 阅读5分钟

什么是闭包?维基百科上对闭包是这样描述的:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如 C++)。

闭包的概念出现于 60 年代,最早实现闭包的程序语言是 Scheme。之后,闭包被广泛使用于函数式编程语言如 ML 语言和 LISP。很多命令式程序语言也开始支持闭包。

Rust 中的闭包(Closure)

Rust 中的闭包(closure)是一种匿名函数,闭包可以赋给变量也可以作为参数进行传递。闭包能够捕获外部作用域中的变量。

在 Rust 中,函数和闭包都是实现了 FnFnMutFnOnce Trait 的类型。

闭包的定义与函数定义类似,使用 || 代替 () 将参数括起来,函数体同样使用 {} 作为边界。

示例

// 函数的定义
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  ; // 函数体中只有一个表达式时,可以省略大括号

类型推断

闭包的参数类型可以省略,编译器会自动推断参数类型,并且以第一次被调用时的推断结果为准。

示例

let example_closure = |x| x;
let s = example_closure(String::from("hello")); // 此时参数 x 的类型确定为 `String`
let n = example_closure(5); //expected struct `String`, found integer rustc(E0308)

在上面的示例中,闭包在第一次调用时编译器推断出它的参数类型是 String,接下来再调用时就不能传入其他类型值了。

捕获变量

闭包可以捕获周围环境中的变量,捕获的方式有三种:

  • 不可变借用
  • 可变借用
  • 获取所有权

示例

fn main() {
    // 获取所有权
    let msg1 = String::from("Hello String!");
    let say_hello = || {
        let msg = msg1;
        println!("{}", msg);
    };
    say_hello(); // Hello String!
    //println!("{}", msg1); // 此时无法访问`msg1`,因为它的所有权已经转移到闭包了

    // 不可变借用
    let msg2 = String::from("Hello &String!");
    let say_hello = || {
        let msg = &msg2;
        println!("{}", msg);
    };
    say_hello(); // Hello &String!
    println!("{}", msg2); // Hello &String!

    // 可变借用
    let mut msg3 = String::from("Hello &mut String");
    let mut say_hello = || {
        let msg = &mut msg3;
        msg.push_str("!");
        println!("{}", msg);
    };
    say_hello(); // Hello &mut String!
    say_hello(); // Hello &mut String!!
    println!("{}", msg3); // Hello &mut String!!
}

另外,如果要强制闭包获取环境中的值的所有权,我们可以使用 move 关键字。

示例: 正常情况下,下面两次打印输出都是 10

fn main() {
    let mut num = 5;

    {
        let mut add_num = |x: i32| {
            num += x;
            println!("num = {}", num);
        };

        add_num(5); // 10
    }

    println!("num = {}", num); // 10
}

使用 move 之后,闭包中的 num 获得了 5 的所有权(通过拷贝)。

fn main() {
    let mut num = 5;

    {
        let mut add_num = move |x: i32| {
            num += x;
            println!("num = {}", num);
        };

        add_num(5); // 10
    }

    println!("num = {}", num); // 5
}

闭包的实现

Rust 中的闭包实际上是 Trait 的语法糖。

每个闭包实例都有一个唯一的 Trait,即:FnFnMutFnOnce

这三个 Trait 对应三种捕获方式:

  • Fn -- 不可变借用
  • FnMut -- 可变借用
  • FnOnce -- 移动(获得变量值的所有权)

这三个 Fn Trait 的定义:

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

pub trait FnMut<Args>: FnOnce<Args> {
    pub extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

pub trait Fn<Args>: FnMut<Args> {
    pub extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

闭包的 || {} 语法是上面 3 个 Trait 的语法糖。Rust 将会为了环境创建一个结构体,impl 合适的 Trait,并使用它。

闭包作为参数

从三个 Trait 的定义中看,这三种 Trait 是继承关系,FnOnce 是顶级父 Trait。假如我们指定 FnOnce 作为参数类型,这说明闭包可能采取这三种捕获方式中的任意一种。

示例一:在结构体中使用 Fn Trait

struct Cacher<T>
where
    T: Fn(u32) -> u32,
{
    calculation: T,
    value: Option<u32>,
}

示例二:在函数中使用 Fn Trait

fn call_with_one<F>(some_closure: F) -> i32
    where F: Fn(i32) -> i32 {

    some_closure(1)
}

let answer = call_with_one(|x| x + 2);

assert_eq!(3, answer);

闭包和函数指针

一个普通函数有点像一个没有环境的闭包,因此,我们可以将这个函数传给闭包参数。

前提是,这个普通函数的函数指针类型 得与闭包的类型保持一致,即:两者的参数类型和返回值需一致。

示例:

fn call_with_one<F>(some_closure: F) -> i32
where
    F: Fn(i32) -> i32,
{
    some_closure(1)
}

// 函数指针类型是:`fn(i32) -> i32`
fn add_one(i: i32) -> i32 {
    i + 1
}

fn main() {
    let answer = call_with_one(add_one);
    println!("answer: {}", answer);

    let closure = |i| i + 1;
    let answer = call_with_one(closure);
    println!("answer: {}", answer);
}

相关资料

Functional Language Features: Iterators and Closures

Closures - Rust By Example

闭包 · RustPrimer

Closures in Rust | Yury Zhauniarovich

rust-book-chinese/Closures