当前位置:   article > 正文

The Rust Programming Language - 第13章 Rust语言中的函数式语言功能:迭代器与闭包 - 13.1 可以捕获其环境的匿名函数

the rust programming language

13 Rust语言中的函数式语言功能:迭代器与闭包

函数式编程风格通常包括将函数作为另一个函数的参数、返回值,将函数作为值赋值给变量,以供后续执行

本章中我们将会介绍以下内容:

闭包:一个可以存储在变量里的类似函数的数据结构

迭代器:一种处理元素序列的方式

如何使用这些功能来改进第十二章的I/O项目

这两个功能的性能(剧透警告:它们的速度超乎你的想象)

我40米的大刀已经饥渴难耐了,让我们攻下这一章!

13.1 可以捕获其环境的匿名函数

Rust闭包其实就是保存进变量或作为参数传递给其他函数的匿名函数,可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算

和函数不同,闭包允许捕获调用者作用域中的值

使用闭包创建行为的抽象

我们来想象一个场景:我们要通过app为健身用户生成一个健身计划,当然这后面的算法会非常复杂,为了演示放方便,我们用一个函数来模拟,而不用考虑具体的实现细节

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

fn simulated_expensive_calculation(intensity:u32)->u32{
     println!("calculating slowly ... ");
     thread::sleep(Duration::from_secs(2));
     intensity
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

“算法”定义好了,让我们再来考虑使用的问题,其实“生成健身计划”这个指令是用户发出的,用户通过输入一定的参数来启动后台对他们来说“透明的”算法

让我们在main函数中实现调用这个算法的代码,我们指定了两个参数simulated_user_specified_value和simulated_random_number的具体值,本来它们是要在用户发出指令时生成的,但是为了演示我们就先定义了

fn main(){

     let simulated_user_specified_value = 10;
     let simulated_random_number = 7;

     generate_workout(
          simulated_user_specified_value,
          simulated_random_number,
     );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

generate函数是我们实现业务的具体逻辑,我们继续对它进行完善。在这里我们使用了一个if else 组合来对参数进行了处理,这些逻辑非常简单,并且我们使用了随机数来控制是否要休息,那我们再顺便执行一下吧

fn generate_workout(intensity:u32,random_number:u32){
     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!(
               "Run for {} minutes",simulated_expensive_calculation(intensity)
               );
          }
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/minigrep`
calculating slowly ... 
Today, do 10 pushups! 
calculating slowly ... 
Next,do 10 situps!
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

使用函数重构

现在让我们来重构一下程序

我们在上面多次使用了函数simulated_expensive_calculation,现在我们把它提取到变量中,如下

fn generate_workout(intensity:u32,random_number:u32){

     let expensive_result =
          simulated_expensive_calculation(intensity);
     if intensity < 25 {
          println!(
               "Today, do {} pushups! ",
               expensive_result
          );
          println!("Next,do {} situps!",
               expensive_result
          );    
     }else {
          if random_number == 3 {
          println!("Take a break today! Remember to stay hydrated!");
     }else {
          println!(
               "Run for {} minutes!",expensive_result
               );
          }
     }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这样的重构虽然使得代码更加精炼、抽象程度更高,但是这个case中所有情况下都需要调用并等待结果

重构使用闭包存储代码

let expensive_closure = |num| {
          println!("calculating slowly ...");
          thread::sleep(Duration::from_secs(2));
          num
     };
  • 1
  • 2
  • 3
  • 4
  • 5

我们来看一下关于闭包的知识点:

闭包定义是 expensive_closure赋值的 = 之后的部分。闭包定义以一对竖线开始,在竖线中指定闭包的参数,参数可以有多个,比如|param1,param2|

参数之后是存放闭包体的大括号,如果闭包体只有一行,则大括号可以省略,大括号后需是分号结尾,这个主要是因为闭包体的最后一行没有分号,跟函数体一样,所以闭包体(num)最后一行的返回值作为调用闭包时的返回值

注意:let语句意味着expensive_closure包含一个匿名函数的定义,而不是调用函数的返回值

为什么我们要使用闭包?是因为我们需要在一个位置定义代码、存储代码、并在之后的位置实际调用它,这个例子中,期望调用的代码存储在expensive——closure中。这个意义上如将函数值赋值给变量

调用闭包就跟调用函数类似

fn main(){

     let simulated_user_specified_value = 10;
     let simulated_random_number = 7;

     generate_workout(
          simulated_user_specified_value,
          simulated_random_number,
     );
}


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:u32,random_number:u32){

     let expensive_closure = |num| {
          println!("calculating slowly ...");
          thread::sleep(Duration::from_secs(2));
          num
     };

     if intensity < 25 {
          println!(
               "Today, do {} pushups! ",
               expensive_closure(intensity)
          );
          println!("Next,do {} situps!",
               expensive_closure(intensity)
          );    
     }else {
          if random_number == 3 {
          println!("Take a break today! Remember to stay hydrated!");
     }else {
          println!(
               "Run for {} minutes!",expensive_closure(intensity)
               );
          }
     }
}
  • 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

但是上述代码还有一个问题就是在第一个if块中调用了两次,这就多耗了一倍的时间。那怎么办呢?可以在if块中创建一个本地变量存放闭包调用结果,不过闭包还提供了另外一种解决方案,我们随后讨论

闭包类型推断和注解

闭包不要求像fn 函数那样在参数和返回值上都注明类型。那为甚么函数需要呢?因为函数是要暴露给用户的,它得让用户了解,而闭包通常很短,只在小范围的上下文中使用,它们不需要暴露给外部用户,当然你非要注明类型也不是不可以,就是比较麻烦,如下,使用类型注解让闭包看上去更像一个函数了

let expensive_closure = |num:u32|->u32 {
          println!("calculating slowly ...");
          thread::sleep(Duration::from_secs(2));
          num
     };
  • 1
  • 2
  • 3
  • 4
  • 5
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   ;
  • 1
  • 2
  • 3
  • 4

让我们再来看一下编译器是怎么推断闭包参数和返回值类型的,我们看到发生了panic,因为在变量s中闭包的参数和返回值已经被推断为字符串类型了

let example_closure = |x| x;

     let s = example_closure(String::from("hello"));//
     let n = example_closure(5);
  • 1
  • 2
  • 3
  • 4
let n = example_closure(5);
   |                              ^
   |                              |
   |                              expected struct `String`, found integer
   |                              help: try using a conversion method: `5.to_string()`
  • 1
  • 2
  • 3
  • 4
  • 5

使用带有泛型和Fn trait的闭包

前面我们挖了一个坑说要解决if块中多次调用闭包耗时的问题,现在我们填上这个坑

我们创建一个存放闭包和调用闭包结果的结构体,该结构体会在需要时执行闭包并缓存结果值,这样其他代码只需要复用结果值就行了,不必要再执行闭包代码了,这种模式被称为memoization或lazy evaluation(惰性求值)

当然为了将闭包放进结构体,我们得指定闭包的类型,每个闭包都有自己独立的类型

为了定义使用闭包的结构体、枚举或函数参数,我们还得使用泛型和trait

fn 系列的trait有标准库提供,所有的闭包都实现了trait Fn、FnMut或者FnOnce中的一个,在“闭包会捕获其环境”中我们再来了解他们的区别,这里用Fn trait就可以

为了满足Fn trait bound, 我们增加了代闭包所必须的参数和返回值类型。在这个例子中,闭包有一个u32的参数并返回一个u32,这样所指定的trait bound 就是Fn(u32)-> u32

struct Cacher<T>
where T: Fn(u32) -> u32
{
     calculation:T,
     value:Option<u32>,
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Casher 的缓存逻辑如下:

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
                    },
               }
          }
     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

我们在generate_workout函数中利用结构体,运行一下,可以成功运行

fn generate_workout(intensity:u32,random_number:u32){

     let mut expensive_result = Cacher::new(|num| {
          println!("calculating slowly ...");
          thread::sleep(Duration::from_secs(2));
          num
     });

     if intensity < 25 {
          println!(
               "Today, do {} pushups! ",
               expensive_result.value(intensity)
          );
          println!("Next,do {} situps!",
               expensive_result.value(intensity)
          );    
     }else {
          if random_number == 3 {
          println!("Take a break today! Remember to stay hydrated!");
     }else {
          println!(
               "Run for {} minutes!",expensive_result.value(intensity)
               );
          }
     }
}
  • 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

Casher实现的限制

值缓存在构建代码时是常规操作,但是目前Casher还存在两个小问题,这限制了我们的使用

Cacher实例假设对于value方法的任何arg参数值总会返回相同的值,也就是说,这个Casher测试会失败

#[test]
fn call_with_different_values(){
     let mut c = Cacher::new(|a|a);

     let v1 = c.value(1);
     let v2 = c.value(2);

     assert_eq!(v2,2);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
running 1 test
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
  left: `1`,
 right: `2`', src/main.rs:103:6
 test call_with_different_values ... FAILED
failures:
failures:
    call_with_different_values

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这是因为Cacher存放了一个值,可以通过存放一个HashMap来处理

第二个限制是只能接受一个u32值并返回u32值的闭包,但是我们可以通过缓存一个字符串slice并返回usize值的闭包结果。这样就可以增加这种结构体的灵活性

闭包会捕获其环境

在上面的例子中,我们把闭包当作内联匿名函数来处理

但闭包还有一个功能:它可以捕获其环境并访问被定义的作用域的变量,如下x在闭包之外,但是它也可以被闭包使用,但是这种捕获内存并且产生额外的开销

fn main(){
     let x =4;
     
     let equal_to_x = |z| z == x;
     
     let y = 4;

     assert!(equal_to_x(y))
  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

但函数就不行了

fn main(){
let x =4;
     
     fn equal_to_x (z:i32)->bool{z == x};
     
     let y = 4;     

     assert!(equal_to_x(y))
  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
can't capture dynamic environment in a fn item
  --> src/main.rs:32:39
   |
32 |      fn equal_to_x (z:i32)->bool{z == x};
   |                                       ^
   |
   = help: use the `|| { ... }` closure form instead
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

闭包通过三种方式捕获其环境,闭包周围的作用域被称为其环境:获取所有权、可变借用和不可变借用。这三种捕获值的方式被编码为如下三个Fn trait

FnOnce消费从周围作用域捕获的变量,只能被调用一次

FnMut获取可变借用值所以可以改变其环境

Fn从环境获取不可变的借用值

当创建一个闭包,Rust会根据其如何使用环境中变量来推断我们希望如何引用环境

在参数列表前使用move关键字可以强制闭包获取其使用的环境值的所有权

fn main(){
let x = vec![1,2,3];

     let equal_to_x = move |z| z == x;

     let y = vec![1,2,3];
     assert!(equal_to_x(y));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

为了展示闭包作为函数参数时捕获其环境的作用,我们接下来学习迭代器

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号