24_rust_闭包

发布时间 2023-11-09 09:43:42作者: 00lab

闭包

函数式编程的风格特点:

  • 函数作为参数
  • 函数作为其他函数的返回值
  • 函数赋值给变量,之后再执行

rust语言也引入了很多函数式编程的语法特性。

闭包的概念

闭包(closure):可捕获其所在环境的匿名函数。

  • 是匿名函数
  • 能够保存为变量、作为参数和返回值
  • 可在一个地方创建闭包,在另一个上下文中调用闭包
  • 可从其定义的作用域捕获值

闭包的定义:

let 闭包名 = |参数1: 可选类型声明, 参数2: 可选类型声明| -> 可选返回值类型 {闭包函数体}; // 整体是一个赋值语句
// 参数类型声明非必要,通常被使用后,编译器能够自动推断

let closure_test = |num: u32| {
  println!("calculate test");
  thread::sleep(Duration::from_secs(2)); //睡眠2s
  num // 函数体返回值
};

闭包的类型推断

和fn定义的函数不同,不强制要求标注参数类型,因为函数是对外暴露的接口,但闭包不是。

  • 闭包不要求标注参数和返回值类型
  • 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
  • 也可手动添加类型标注
let closure_test = |num: u32| -> u32 {
  println!("calculate test");
  thread::sleep(Duration::from_secs(2)); //睡眠2s
  num // 函数体返回值
};

函数和闭包的定义语法:

fn test_func(x: u32) -> u32 { x + 1 } //函数

let test1 = |x: u32| -> u32 { x + 1 }; //完整闭包
let test2 = |x|             { x + 1 }; //闭包可省略类型标注
let test3 = |x|               x + 1  ; //闭包只有一个表达式时还可省略大括号

注:闭包的定义最终只为参数/返回值推断出唯一的具体类型,存在多次使用但类型不同的情况则报错。

let closure1 = |x| x;
let s = closure1(String::from("t")); //本行执行后推断出x的类型是String,并绑定至闭包,后不可再变
let n = closure1(5); // 报错,5是整数类型,但闭包已确定是string类型,类型不匹配

使用泛型参数和Fn Trait存储闭包

创建一个struct,持有闭包及调用结果,只在需要结果时才执行闭包,且能缓存结果,这种模式叫记忆化(memoization)或延迟计算(lazy evaluation)
Fn Trait:
由标准库提供,所有闭包都至少实现了Fn Trait、FnMut Trait、FnOnce Trait三个之一。
例子:希望只调用一次耗时闭包

use std::thread;
use std::time::Duration;

struct Cacher<T> // 泛型T表示闭包的类型
    where T: Fn(u32) -> u32, // T的约束是接收u32类型及返回值也是u32类型,类似于函数指针类型定义
{
    calc: T,
    v: Option<u32>,
}
impl<T> Cacher<T>
    where T: Fn(u32) -> u32,
{
    fn new(calc: T) -> Cacher<T> {
        Cacher {
            calc,
            v: None,
        }
    }
    fn value(&mut self, arg: u32) -> u32 {
        match self.v {
            Some(v) => v,
            None => {
                let v = (self.calc)(arg);
                self.v = Some(v);
                v
            }
        }
    }
}
fn gen_data(a: u32, rand_num: u32) {
    let mut closure1 = Cacher::new(|num| {
        println!("calc...");
        thread::sleep(Duration::from_secs(2));
        num
    });
    if rand_num < 5 {
        println!("test multi {}{}", closure1.value(a), closure1.value(a)); //只会调用一次闭包
    } else {
        println!("test multi {}{}{}", closure1.value(a), closure1.value(a), closure1.value(a+2)); //只会调用一次闭包
    }
}
fn main() {
    gen_data(3, 2);
    gen_data(5, 6);
}

不过上面代码可看出,多次调用value传入不同参数时,获得的值一样的,因为第一次计算后v有值了就不再调用闭包了。这种情况可采用HashMap代替当个值的方式解决,当key是传入的参数,value是执行闭包的结果,当key不存在时就会调用一次闭包。

闭包捕获上小文

就是闭包能够捕获其所在上下文的变量,但函数不行。

fn main() {
    let x = 5;
    let closure_test = |z: u32| z == x; // 能够捕获当前作用域内的x变量
    fn test_func(z: u32) { // 试图定义一个函数捕获
        z == x // 编译报错can't capture dynamic environment in a fn item
    }
}

捕获环境信息时会产生内存开销。

闭包从所在环境捕获值的方式
与函数获得参数的三种方式一样:

  • 1 取得所有权:FnOnce
  • 2 可变借用:FnMut
  • 3 不可变借用:Fn

创建闭包时,通过闭包对环境值的使用,rust推断出具体使用哪个trait:

  • 所有的闭包都实现了FnOnce
  • 没有移动捕获变量的实现了FnMut
  • 无需可变访问捕获变量的闭包实现了Fn

move关键字

在参数列表前使用move关键字,可强制闭包取得所使用的环境值的所有权,当闭包传递新线程以移动数据使其归新线程所有事,常用此技术。

fn main() {
    let x = 5;
    let closure_test = move |z: u32| z == x;
    println!("move after{:?}", x);
    println!("{:?}", closure_test(3));
}

在教程中会报borrow of moved value:x,variable moved due to use in closure的错误,但本人实际测试未抱错,能正常运行。