Rust | 04. 迭代器与闭包、代码发布、智能指针、多线程

  • Rust在设计过程中也受函数式编程的影响(将函数作为变量、参数或返回值的思想),闭包就是其产物之一

  • 简单而言,闭包是可以捕获其所在环境的匿名函数,主要有如下几个特征:是匿名函数,可以保存为变量或作为参数,可以在一个地方创建另一个上下文中调用,可以从其定义的作用域捕获值

  • 看如下例子,要求程序根据用户的健康参数动态地生成运动推荐,在该实现中,假定计算过程需要几秒钟时间才能完成,程序当下的目标是不让用户发生不必要的等待,仅在必要时调用该算法一次

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    use std::thread;
    use std::time::Duration;
    
    fn simulated_expensive_calculation(intensity: u32) -> u32 {
      println!("calculating slowly...");
      thread::sleep(Duration::from_secs(2));
      intensity
    }
    
    fn generate_workout(intensity: i32, random_number: u32) {
      // let expensive_result = simulated_expensive_calculation(intensity);
      if intensity < 25 {
        println!(
          "Today, do {} pushups!", 
          simulated_expensive_calculation(intensity)
        );
        println!(
          "Next, do {} situps!", 
          simulated_expensive_calculation(intensity)
        )
      } else {
        if random_number == 3 {
          println!("Take a break today! Remember to stay hydrated!");
        } else {
          println!(
            "Today, run for {} minutes!", 
            simulated_expensive_calculation(intensity)
          );
        }
      }
    }
    
  • 针对该代码,最常见的处理思路是用一个变量,将这个函数在最开始运行一次,并将结果保存起来。但这样同样会有问题,就是说,有一种情况下,程序可以根本不执行该程序,这样做并不能完美达成仅必要时执行的目的。如果有一种方法,先把函数定义好,但不执行,只有等到真正需要这个结果时才执行,就可以解决这个问题。

  • 如果可以有这样一种结构体,其可以持有一个用户输入的、可以变化的函数,并且当该函数的调用一次后可以缓存其结果,这样就可以做到只在需要结果时,执行一次并缓存结果,下次使用只需要判断这个值是否存在就可以。这个模式叫做记忆化(memoization)或延迟计算(lazy evaluation)。这种解决思路就需要用到闭包可以作为参数,输入并保存在结构体中的特点。

  • 闭包的定义方法如下let res = |parameters|{function body};,这里等号左边为函数的执行结果,等号的右边为一个匿名函数,参数用|包裹,但是不强制要求标注参数和返回值的类型,其通常很短,在狭小的上下文中工作,编译器通常能推断出类型。此外,其存在与变量中,甚至不需要命名,就不会暴露给外部用户。函数体的写法和其他地方是一样的。在当下函数的定义阶段,其并不会执行,res值为暂时为函数的定义,其只有在遇到(arguements)时才会执行。

    1
    2
    3
    4
    
    fn  add_1 (x: u32) -> u32 { x+1 }
    let add_2=|x :u32| -> u32 { x+1 };
    let add_3=|x|             { x+1 };
    let add_4=|x|               x+1  ;
    
  • 需要注意的时,闭包的定义最终只会为参数/返回值推断出唯一具体的类型。如果第3、4行都不存在,即闭包未被调用时,函数会报错,因为编译器无法推断出其类型;当程序读到第3行时,闭包的参数类型就被界定为字符串类型,第4行会报错。

    1
    2
    3
    4
    5
    
    fn main() {
      let example_closure = |x| x;
      let s = example_closure(String::from("hello"));
      // let n = example_closure(5); //会报错
    }
    
  • 另外,对结构体的定义需要知道所有字段的类型,也同样需要指明闭包的类型。每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样,其类型也是不一样的。因此,为了能够在结构体中存储闭包,就需要使用到泛型以及Trait Bound。在标准库中提供了一些Fn Trait,所有的闭包都需要至少实现其中的一种(FnFnMutFnOnce)。

  • 上面问题的代码可以做如下修改:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    
    use std::thread;
    use std::time::Duration;
    
    struct Cacher<T>
      where T: Fn(u32) -> u32
    {
      calculation: T,
      value: Option<u32>,
    }
    
    impl<T> Cacher<T> 
      where T: Fn(u32) -> u32
    {
      fn new(calculation: T) -> Cacher<T> {
        Cacher {
          calculation,
          value: None,
        }
      }
    
      fn value(&mut self, arg: u32) -> u32  {
        match self.value {
          Some(v) => v,
          None => {
            let v = (self.calculation)(arg);
            self.value = Some(v);
            v
          }
        }
      }
    }
    
    fn generate_workout(intensity: i32, random_number: u32) {
      let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        intensity    
      });
    
      if intensity < 25 {
        println!(
          "Today, do {} pushups!", 
          expensive_result(intensity)
        );
        println!(
          "Next, do {} situps!", 
          expensive_result(intensity)
        )
      } else {
        if random_number == 3 {
          println!("Take a break today! Remember to stay hydrated!");
        } else {
          println!(
            "Today, run for {} minutes!", 
            expensive_result(intensity)
          );
        }
      }
    }
    
  • 代码在修改后已经可以达成最初的目的了,但是仍旧存在问题。当前的缓存器(Cacher)实例只使用第一次调用时的参数,并只得到一个结果,并且输入的类型和输出的类型需要一致。可以这样解决,

    • 将结果value定义为一个HashMap,其根据输入的参数来获取结果,当结果不存在时再进行计算
    • 输入和输出也可以使用两个不同的泛型表示方法来实现拓展
  • 闭包的另一个特点是其可以访问其定义位置作用域内的变量,而其他常规函数则没有这个能力。看如下例子,当需要判断环境中某个特定值作为函数定义的一部分时,上下文环境中的变量是不可以进入到函数定义作用域内的。需要注意的是,闭包在捕获周围变量时,是会有额外内存开销的,这个也跟普通函数不同。

    1
    2
    3
    4
    5
    6
    7
    
    fn main() {
      let x = 4;
      let equal_to_x = |z| z==x;
      // fn equal_to_?(z: i32) -> bool { z==x? }
      let y = 4;
      assert!(equal_to_x(y));
    }
    
  • 闭包从周围环境中捕获值的三种方式:

    • 取得所有权 FnOnce:即闭包可以获得定义处周围变量的所有权,并将其消耗掉
    • 可变借用 FnMut:其从环境中借用值,且可修改
    • 不可变借用 Fn:从环境中借用值,但不可修改
  • 在创建闭包时,通过闭包对环境值的使用,Rust就能推断出具体是使用了哪个Trait;具体而言,所有必要都至少会被调用一次,因而一定都实现了FnOnce,余下的,没有移动捕获变量的实现了FnMut,无需可变访问捕获变量的闭包则实现了Fn。其三者存在包含关系如下:(FnOnce(FnMut(Fn)))(不理解先记下来)

  • 在参数列表前使用move关键字,可以强制闭包取得它所使用的环境值的所有权。当将闭包传递给新线程以移动数据使其归新线程所有时,这个技术就很有用。

    1
    2
    3
    4
    5
    6
    7
    
    fn main() {
      let x = vex![1, 2, 3];
      let equal_to_x = move |z| z==x;
      println!("can't use x here", x);
      let y = vec![1, 2, 3];
      assert!(equal_to_x(y));
    }
    
  • 当需要指定FnTrait Bound时,最好先使用Fn,然后编译器会告诉你能不能行,或者需要其他类型的