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
}

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

让我们在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,);
}

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));}}
}
cargo runFinished dev [unoptimized + debuginfo] target(s) in 0.07sRunning `target/debug/minigrep`
calculating slowly ...
Today, do 10 pushups!
calculating slowly ...
Next,do 10 situps!

使用函数重构

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

我们在上面多次使用了函数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);}}
}

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

重构使用闭包存储代码

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

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

闭包定义是 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));}}
}

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

闭包类型推断和注解

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

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

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

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

使用带有泛型和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>,
}

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},}}}

我们在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));}}
}

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);
}
running 1 test
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`left: `1`,right: `2`', src/main.rs:103:6test call_with_different_values ... FAILED
failures:
failures:call_with_different_valuestest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s

这是因为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))}

但函数就不行了

fn main(){
let x =4;fn equal_to_x (z:i32)->bool{z == x};let y = 4;     assert!(equal_to_x(y))}
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

闭包通过三种方式捕获其环境,闭包周围的作用域被称为其环境:获取所有权、可变借用和不可变借用。这三种捕获值的方式被编码为如下三个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));
}

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

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

  1. 17.Rust中函数式语言功能:迭代器与闭包

    Rust 的设计灵感来源于很多现存的语言和技术.其中一个显著的影响就是 函数式编程(functional programming).函数式编程风格通常包含将函数作为参数值或其他函数的返回值.将函数赋值 ...

  2. The Rust Programming Language - 第19章 高级特征 - 19.5 宏

    19 高级特征 我们将在这一章学习更多高级功能 19.5 宏 宏指的是Rust中一系列功能,宏用macro_rules!来声明 三种过程宏: 1.自定义#[derive]宏在结构体和枚举上指定通过de ...

  3. The Rust Programming Language - 第18章 模式与模式匹配 - 18.2 Refutability(可反驳性):模式是否会匹配失效

    18 模式与模式匹配 模式是Rust中的特殊语法,用来匹配类型中的结构,无论类型复杂与否.模式由以下一些内容组合而成: 字面值\解构的数组.枚举.元组或者结构体\变量\通配符\占位符,这些部分描述了我 ...

  4. The Rust Programming Language - 第11章 测试 - 11.1 编写测试

    11 测试 编写自动化测试 程序的正确性代码如我们期望的那样运行,Rust也在语言本身包含了编写软件测试的支持 本章我们会讲到编写测试时用到的注解和宏,运行测试的默认行为和选项,以及如何将测试组织成单 ...

  5. The Rust Programming Language - 前言

    前言 Rust程序设计语言本质在于赋能 Rust语言会涉及"系统层面"的工作,设计内存管理.数据表示和并发等底层细节(其实就是一些计算机系统.组成原理.数据结构.网络等方面的基础知 ...

  6. 13、C语言中的单引号和双引号

    -- C语言中的单引号用来表示字符字面量,编译为对应的ASCII码 -- C语言中的双引号用来表示字符串字面量,编译为对应的内存地址 'a'表示字符字面量,在内存中占一个字节,'a'+1表示'a'的A ...

  7. 《The C Programming Language》读书笔记 说明

    <The C Programming Language>读书笔记 说明 作为笔记而言,完全是一种自写自看的行为,本来是没有必要写这篇东西的.但是作为一个生活在网络时代的学 生来说,想学好一 ...

  8. Rust Closure 闭包解析(匿名函数)

    Rust Closure 闭包解析(匿名函数) 文章目录 Rust Closure 闭包解析(匿名函数) 正文 1. 简单闭包 - 纯粹的匿名函数 2. 捕获上下文 & FnOnce.FnMu ...

  9. C语言z(1 3),C语言练习题z(1-3章).doc

    C语言练习题z(1-3章) 氨谐择澜略珠杂翅镰藕容昭知喻僳敛远求妇肛客谈悦裙悼缚焚缆够鬼窒锐撮亦沾毛赠迎蔗茸幂厦廓敖卸渗睬润荫诞涅鉴血婪毯劣诉娇谈立握忆圃毙饥户仆吃金木髓怯蝎蛊辰囤阔菌藏炯滨葬桌壮顷罪 ...

最新文章

  1. 干货!高容错微服务架构设计思路
  2. 找工作刷题--------Java相关
  3. mysql_upgrade 升级_采用MySQL_upgrade升级授权表方式升级
  4. 最小二乘算法MATLAB代码实现
  5. mybatis resultmap嵌套_Java面试专题之九:Mybatis面试5个大概率被问到的问题
  6. matlab gpu deep learning_在Matlab中使用tensorflow (1)
  7. 去IOE:去掉“IE”就Ok?
  8. C/C++笔试、面试题(上)
  9. Azure Lambda Function创建失败 - 400 bad request和成功 - 201 Created
  10. 【dfs】虫食算(ybtoj dfs-1-3)
  11. 【计算机网络复习】1.2.1 分层结构、协议、接口、服务
  12. python资源管理器选择文件_Python:在资源管理器中获取选定文件的列表(windows7)...
  13. CentOS 6.X启动流程
  14. input属性disabled和readonly的区别
  15. WSS 3.0部署备忘 六
  16. css3 - 图标元素动画效果1 - 只执行一次动画
  17. linux mint(ubuntu)频率锁定解决
  18. 空气净化器UL867测试报告流程
  19. 2020计算机数电实验第四次(2)
  20. 湖人vs马刺第一场艰难取胜

热门文章

  1. html 淡出淡入轮播图,用CSS3 transition属性实现淡入淡出轮播图
  2. 实验项目名称:微波技术与天线CST仿真实验
  3. c语言结构体张三丰,第五次视频我发到网易云课堂上了
  4. WPF工控组态软件之管道和冷却风扇开发
  5. Kotlin基础语法之 ==和===的区别
  6. IM开发技术学习:揭秘微信朋友圈这种信息推流背后的系统设计
  7. 因子分析 factor analysis (七) :因子分析法与主成分分析的异同
  8. 数据库,SQL语句的介绍
  9. 初学者备战蓝桥杯历程(大学编程学习历程记录,题目思路献给需要备考蓝桥杯的同学)
  10. Java树形菜单的构建、遍历以及获取树形菜单的Id集合List