Rust 支持函数式编程,因此高阶函数、闭包等特性都不可或缺。

本文主要参考了 Rust Course 和 Rust 标准库文档。

什么是闭包

闭包是在支持头等函数的编程语言中实现词法绑定的一种技术,与函数不同的是,它可以捕捉自由变量,这意味着即使脱离了闭包创建时的上下文也能正常运行。

闭包的用途:

  • 闭包实现了类似环境变量的功能,多个函数使用相同的环境。

  • 闭包可以定义自己的控制流程。

  • 闭包可以实现对象系统。

在 Rust 中,可以像如下方式创建一个最简单的闭包:

let add1 = |x: i32| x + 1;
assert_eq!(add1(1), 2);

上面这段代码创建了一个匿名函数add1并在之后调用。

Rust 中闭包的特点:

  • 参数列表中使用||代替()

  • 如果只有一个语句可以省略掉{}

  • 可以捕捉自由变量。

可以利用闭包的类型自动推导,以下几种方法是等效的:

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  ;

需要注意的是,Rust 虽然有类型的自动推导,但是它只是一种语法糖,而不是泛型,当编译器推导出一种类型的闭包之后,它会一直使用该类型。

闭包本质上是一种词法绑定,故当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

三种Fn特征

闭包有三种方式捕获自由变量,分别是:转移所有权、可变借用和不可变借用:

  • FnOnce,顾名思义,这种闭包只能使用一次,因为它会夺取捕获到的变量的所有权。
pub trait Fn<Args> : FnMut<Args> {
    extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
  • FnMut,使用可变引用捕获了环境中的值,因此可以对其进行修改。
pub trait FnMut<Args> : FnOnce<Args> {
    extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
  • Fn,使用不可变引用捕获了环境中的值。
pub trait FnOnce<Args> {
    type Output;
    extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

当闭包的生命周期大于需要捕获的自由变量的生命周期时,可以使用move关键字将自由变量的所有权移入闭包中:

let v = vec![1, 2, 3];
let print_vec = move || println!("{:?}", v);

除了FnMut需要在创建变量时使用mut,创建闭包时并不需要显式地指定它实现的特征,因此闭包的类型实际上是编译器推导出来的,只有将闭包当作参数传递需要类型标注时才需要显式标记类型。

一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。

  • 所有的闭包都自动实现了FnOnce特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所捕获变量的所有权的闭包自动实现了FnMut特征
  • 不需要对捕获变量进行改变的闭包自动实现了Fn特征

下面是 Rust Course 中的一个例子,说明了闭包实现的特征取决于使用自由变量的方法,而且因为只使用了不可变引用,三种特征都可以实现。

fn main() {
    let s = String::new();

    let update_string =  || println!("{}",s);

    exec(update_string);
    exec1(update_string);
    exec2(update_string);
}

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

fn exec1<F: FnMut()>(mut f: F)  {
    f()
}

fn exec2<F: Fn()>(f: F)  {
    f()
}

在编程中需要我们善于利用编译器的报错,可以先使用 Fn 特征,然后编译器会告诉你正误以及该如何选择。

闭包作为返回值

闭包作为函数返回值时不要忘了使用impl关键字,可以理解为实现了Fn(xxx) -> yyy特征。

fn add_num(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}
fn main() {
    let add1 = add_num(1);
    assert_eq!(add1(0), 1);
}

使用这种方式有一个局限性就是只能返回一种闭包,因为即使是相同函数签名的闭包也是不同类型的,这时就需要使用特征对象:

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {
    let num = 5;

	// 要注意闭包中只捕获了 num 而没有捕获 x,x是闭包的参数,而不是上文中的 x
    if x > 1{
        Box::new(move |x| x + num)
    } else {
        Box::new(move |x| x - num)
    }
}

fn main() {
    let add5 = factory(2);
    let sub5 = factory(1);
    assert_eq!(add5(0), 5);
    assert_eq!(sub5(0), -5);
}

闭包的生命周期

Rust 中函数和闭包有不同的生命周期省略原则(Lifetime Elision),故两段几乎相同的代码在函数中能编译通过,在闭包中不能通过:

fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };

会报错:

error: lifetime may not live long enough
 --> d.rs:2:47
  |
2 |     let closure_slision = |x: &i32| -> &i32 { x };
  |                               -        -      ^^ returning this value requires that `'1` must outlive `'2`
  |                               |        |
  |                               |        let's call the lifetime of this reference `'2`
  |                               let's call the lifetime of this reference `'1`

如果函数参数中只有一个引用类型,那该引用的生命周期会被自动分配给所有的返回引用。但是闭包不会这样,这点值得我们注意。

结构体中使用闭包

如下代码是官方示例的升级版,Cacher结构体只有在需要获取值且尚未重复计算时执行闭包,在执行后将计算结构缓存到一个HashMap之中,这种模式被叫做惰性求值。

use std::collections::HashMap;

struct Cacher<F>
where F: Fn(i32) -> i32
{
    val_map: HashMap<i32, i32>,
    calculation: F
}

impl <F> Cacher<F>
where F: Fn(i32) -> i32
{
    fn new(calculation: F) -> Cacher<F> {
        Cacher {
            val_map: HashMap::new(),
            calculation
        }
    }

    fn value(&mut self, arg: i32) -> i32 {
        if let Some(v) = self.val_map.get(&arg) {
            *v
        } else {
            let v = (self.calculation)(arg);
            self.val_map.insert(arg, v);
            v
        }
    }
}

fn main() {
    let mut c = Cacher::new(|x| x);
    let v1 = c.value(1);
    let v2 = c.value(2);
    assert_eq!(v1, 1);
    assert_eq!(v2, 2);
}