3.13 闭包

3.13.1 闭包介绍

闭包是可以保存进变量或者作为参数传递给其它函数的匿名函数。闭包和函数不同的是,闭包允许捕获调用者作用域中的值。下面为使用闭包的简单示例:

fn main() {
    let use_closure = || {
        println!("This is a closure");
    };
    use_closure(); // 此行打印“This is a closure”
}

闭包有如下语法格式:

#![allow(unused)]
fn main() {
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;                            //自动推导参数类型和返回值类型
}

闭包定义会为每个参数和返回类型推导一个具体类型,但是不能推导两次。下面是错误示例:

fn main() {
    let example_closure = |x| x;
    let s = example_closure(String::from("hello"));
    let n = example_closure(5); //报错,尝试推导两次,变成了不同的类型
}

3.13.2 闭包捕获环境

下面的示例展示了闭包捕获环境中的变量:

fn main() {
    let x = 4;
    let equal_to_x = |z| z == x; //捕获环境中的值
    let y = 4;
    assert!(equal_to_x(y));
}

闭包可以通过三种方式捕获其环境,对应函数的三种获取参数的方式,分别是获取所有权、可变借用和不可变借用。 这三种捕获值的方式被编码为如下三个trait:

  • FnOnce:消费从周围作用域捕获变量(即获取捕获变量的所有权),闭包周围的作用域被称为其环境。为了消费捕获到的变量,闭包必须获取其所有权并将其移动进闭包。其名称的Once部分代表了闭包不能多次获取相同变量的所有权。
  • FnMut:获取可变的借用值,所以可以改变其环境。
  • Fn:从其环境获取不可变的借用值。

当创建一个闭包时,Rust会根据其如何使用环境中的变量来推断如何引用环境。由于所有闭包都可以被调用至少一次,因此所有闭包都实现了FnOnce。没有移动被捕获变量的所有权到闭包的闭包也实现了FnMut,而不需要对捕获的变量进行可变访问的闭包则实现了Fn

下面示例分别给出了实现三种Trait的闭包:

fn call_once(c: impl FnOnce()) {
    c();
}
fn call_mut(c: &mut impl FnMut()) {
    c();
}
fn call_fn(c: impl Fn()) {
    c();
}
fn main() {
    // 1、闭包use_closure1只实现了FnOnce Trait,只能被调用一次
    let s = "Hello".to_string();
    let use_closure1 = move || {
        let s1 = s;
        println!("s1 = {:?}", s1);
    };
    use_closure1(); // 此行打印“s1 = "Hello"”
                    // println!("s = {:?}", s); // 编译错误:因为s所有权已经被移动闭包中use_closure1中
                    // use_closure1();  // 编译错误:多次调用use_closure1出错
    let s = "Hello".to_string();
    let use_closure11 = move || {
        let s1 = s;
        println!("s1 = {:?}", s1);
    };
    call_once(use_closure11);

    // 2、闭包use_closure2只实现了FnOnce Trait和FnMut Trait
    let mut s = "Hello".to_string();
    let mut use_closure2 = || {
        s.push_str(", world!");
        println!("s = {:?}", s);
    };
    use_closure2(); // 此行打印“s = "Hello, world!"”
    use_closure2(); // 可以多次调用,此行打印“s = "Hello, world!, world!"”
    call_mut(&mut use_closure2);
    call_once(use_closure2);

    // 3、闭包use_closure3实现了FnOnce Trait、FnMut Trait和Fn Trait
    let s = "Hello".to_string();
    let mut use_closure3 = || {
        println!("s = {:?}", s);
    };
    use_closure3(); // 此行打印“s = "Hello"”
    use_closure3(); // 可以多次调用,此行打印“s = "Hello!"”
    call_fn(use_closure3);
    call_mut(&mut use_closure3);
    call_once(use_closure3);
}

3.13.3 作为参数和返回值

1. 函数指针

函数指针的使用可以让函数作为另一个函数的参数。函数的类型是fnfn被称为函数指针。指定参数为函数指针的语法类似于闭包。

fn add_one(x: i32) -> i32 {
    x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {  //第一个参数为函数指针
    f(arg) + f(arg)
}
fn main() {
    let answer = do_twice(add_one, 5);
    println!("The answer is: {}", answer);
}

函数指针实现了闭包的三个trait(FnFnMutFnOnce),函数指针作为参数的地方也可以传入闭包。

2. 闭包作为参数和返回值

基于上面的1的知识可知,闭包可以作为参数,同样也可以作为返回值,闭包作为参数的示例如下:

fn wrapper_func<T>(t: T, v: i32) -> i32
where
    T: Fn(i32) -> i32,
{
    t(v)
}

fn func(v: i32) -> i32 {
    v + 1
}

fn main() {
    let a = wrapper_func(|x| x + 1, 1); // 闭包作为参数
    println!("a = {}", a);

    let b = wrapper_func(func, 1);  // 函数作为参数
    println!("b = {}", b);
}

闭包作为返回值的示例如下:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {  // 返回的是trait对象
    Box::new(|x| x + 1)
}

fn main() {
    let c = returns_closure();
    println!("r = {}", c(1)); //等价于println!("r = {}", (*c)(1));
}

需要注意的是,函数定义时返回的是Box包含的trait对象,因为编译器在编译时需要知道返回值的大小。所以对于下面两种returns_closure函数定义,编译器将报错:

#![allow(unused)]
fn main() {
// 错误方式一
fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}
// 错误方式二
fn returns_closure() -> Fn(i32) -> i32 {
    |x| x + 1
}
}

3. 闭包和泛型

闭包还可以和泛型结合在一起使用,示例如下:

// T 要求实现Fn
fn returns_closure1<T>(f: T) -> T
where
    T: Fn(i32) -> i32,
{
    f
}
// T 要求实现FnMut
fn returns_closure2<T>(f: T) -> T
where
    T: FnMut(),
{
    f
}
// T 要求实现FnOnce
fn returns_closure3<T>(f: T) -> T
where
    T: FnOnce(),
{
    f
}

fn main() {
    let closure1 = |x| x + 1;
    let c = returns_closure1(closure1);
    println!("r = {}", c(1));

    // T 实现了FnMut、FnOnce
    let mut s = "Hello".to_string();
    let closure2 = || {
        s.push_str(", world!");
    };
    let mut c = returns_closure2(closure2);
    c();
    println!("s: {:?}", s);

    let s = "Hello".to_string();
    let closure3 = move || {
        let s1 = s;
        println!("s = {:?}", s1);
    };
    let c = returns_closure3(closure3);
    c();
}

3.13.4 闭包背后的原理

Rust中的闭包是通过一个特殊的结构体实现的。具体来说,每个闭包都是一个结构体对象,其中包含了闭包的代码和环境中捕获的变量。这个结构体对象实现了一个或多个trait,以便可以像函数一样使用它。 当定义一个闭包时,Rust编译器会根据闭包的代码和捕获的变量生成一个结构体类型,这个结构体类型实现了对应的trait。例如,以下代码定义了一个闭包add_x并调用它:

fn main() {
    let x = 10;
    let add_x = |y| x + y;     // 闭包
    println!("{}", add_x(5));  // 调用闭包
}

在编译时,Rust编译器会将这个闭包add_x转换为如下的结构体类型:

#![allow(unused)]
fn main() {
struct Closure<'a> {
    x: i32,
}

impl<'a> FnOnce<(i32,)> for Closure<'a> {
    type Output = i32;
    fn call_once(self, args: (i32,)) -> i32 {
        self.x + args.0
    }
}

impl<'a> FnMut<(i32,)> for Closure<'a> {
    fn call_mut(&mut self, args: (i32,)) -> i32 {
        self.x + args.0
    }
}

impl<'a> Fn<(i32,)> for Closure<'a> {
    extern "rust-call" fn call(&self, args: (i32,)) -> i32 {
        self.x + args.0
    }
}
}

当闭包被调用时,它实际上是通过调用结构体的方法来执行的。所以调用闭包的代码就变成了如下:

fn main() {
    let x = 10;
    let mut add_x = Closure { x, y: 0 };
    println!("{}", add_x(5));
}