【Rust自学】13.3. 闭包 Pt.3:使用泛型参数和fn trait来存储闭包

embedded/2025/1/18 23:34:21/

13.3.0. 写在正文之前

Rust语言在设计过程中收到了很多语言的启发,而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。

在本章中,我们会讨论 Rust 的一些特性,这些特性与许多语言中通常称为函数式的特性相似:

  • 闭包(本文)
  • 迭代器
  • 使用闭包和迭代器改进I/O项目
  • 闭包和迭代器的性能

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

13.3.1. 回顾

还记得在 13.1 中的例子吗:

做一个程序,根据人的身体指数等因素生成自定义的运动计划。这个程序的算法逻辑并不是重点,重点是算法在计算过程中会花费几秒的时间。我们的目标是不让用户发生不必要的等待。具体来说就是仅在必要的时候才调用该算法,而且只调用一次。

当时我们修改代码为:

rust">use std::thread;  
use std::time::Duration;  fn main() {  let simulated_user_specified_value = 10;  let simulated_random_number = 7;  generate_workout(  simulated_user_specified_value,  simulated_random_number,  );  
}  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!("Today, run for {} minutes!", expensive_closure(intensity));  }  }  
}

但是还存在一个问题:这么写没有解决闭包重复调用的问题。intensity小于25的情况下调用了2次闭包。

对于这个问题,一个解决方案是把闭包的值赋给某个本地变量,让这个本地变量被输出语句重复调用。这么写问题的是会造成一些代码的重复。

所以这里更适合使用另一种解决方法:创建一个结构体,它持有闭包及其调用结果。也就是说,在第一次调用闭包后把结果存到闭包里,如果以后还要调用闭包就直接使用存在里面的结果。它的效果是只会在需要结果时才执行该包,而且可缓存结果。

这种模式通常叫 记忆化(memorization)延迟计算(lazy evaluation)

13.3.2. 让结构体持有闭包

根据刚才的解决方法,目前的问题在于如何让结构体持有闭包。

结构体的定义需要知道所有字段的类型,所以如果想在结构体内存储闭包,就必须指明闭包的类型。

每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样,这两个实例仍然是两个类型。所以存储闭包需要使用泛型以及trait bound(泛型和trait bound的内容在 10.4. trait Pt.2 中有讲,推荐看看这篇)

13.3.3. Fn trait

Fn trait由标准库提供。所有的闭包都至少实现了以下Fn trait之一:

  • Fn
  • FnMut
  • FnOnce

这三个Fn trait间的区别会在下一篇文章讲到。在本例中使用Fn就可以了

知道这些之后就可以修改例子了。首先创建一个结构体:

rust">struct Cache<T: Fn(u32) -> u32>  
{  calculation: T,  value: Option<u32>,  
}
  • 这个结构体有一个泛型参数T,由于它代表的是闭包的类型,它的约束是Fn trait(在本例中使用Fn就可以了),然后参数和返回值是u32,所以写Fn(u32) -> u32
  • 闭包所在的字段是calculation,它的类型就是T
  • 要缓存的值在value字段上,其类型是u32,但是要注意的是不清楚这个值是否已经计算出来并缓存在里面了,所以要用Option类型来包裹,也就是Option<u32>

先在结构体上写一个构造函数用于创建实例:

rust">impl<T: Fn(u32) -> u32> Cache<T> {  fn new(calculation: T) -> Cache<T> {  Cache {  calculation,  value: None,  }  }  
}

这么写看着有点乱,可以用where字句重写一下:

rust">impl<T> Cache<T>   
where   
T: Fn(u32) -> u32  
{  fn new(calculation: T) -> Cache<T> {  Cache {  calculation,  value: None,  }  }  
}

然后,为了实现value有值就取value下的值,valueNone就计算的功能,再写一个函数:

rust">fn value(&mut self, arg: u32) -> u32 {  match self.value {  Some(v) => v,  None => {  let v = (self.calculation)(arg);  self.value = Some(v);  v  }  }  
}

如果实例的value字段有值就返回这个值,没有值就计算出这个值,存储在value字段里再返回。

这部分写好之后,就该把generate_workout的写法改一下,转为使用cache结构体:

rust">fn generate_workout(intensity: u32, random_number: u32) {  let mut expensive_closure = Cache::new(|num|{  println!("calculating slowly...");  thread::sleep(Duration::from_secs(2));  num  });  if intensity < 25 {  println!("Today, do {} pushups!", expensive_closure.value(intensity));  println!("Next, do {} situps!", expensive_closure.value(intensity));  } else {  if random_number == 3 {  println!("Take a break today! Remember to stay hydrated!");  } else {  println!("Today, run for {} minutes!", expensive_closure.value(intensity));  }  }  
}
  • expensive_closure作为Cache结构体的实例,使用new函数把闭包传进去。这里把expensive_closure加上mut设为可变函数是因为后文调用时可能会改变value这个字段的值。
  • 下文所有要使用值的操作都使用value方法来获取。

13.3.4. 使用缓存器实现的限制

这里的Cache字段就是缓存器,用于缓存某个值,但这么写是有限制的。

我把Cache的声明和其方法的代码贴在这里:

rust">struct Cache<T: Fn(u32) -> u32>  
{  calculation: T,  value: Option<u32>,  
}  impl<T> Cache<T>   
where   
T: Fn(u32) -> u32  
{  fn new(calculation: T) -> Cache<T> {  Cache {  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  }  }  }  
}

value这个方法总会得到同样的值:如果value字段没有值,那它就会计算出值然后把值存储在value字段里,之后的其他地方使用value就会得到最开始的计算的这个值,不论传进去的参数是什么。

这么说可能有点模糊,那来看个例子:

rust">fn call_with_different_values(){let mut c = Cache::new(|a| a);let v1 = c.value(1);let v2 = c.value(2);
}
  • cCache的一个实例,传进去了一个闭包。

  • let v1 = c.value(1);这一行时原本cvalue字段没有值,这时候传进去个1,value字段就变成Some(1)了(value字段是Option类型)

  • let v2 = c.value(2);这一行时由于value字段原本有值,所以会直接取value字段的1赋给v2,即使这行的value方法的参数与上一行不一样。

如果不想要这样,就得使用HashMap来代替单个的值,把HashMap的key作为value方法传进去的参数args;而值就作为执行闭包的结果。比如说:

rust">struct ForFun<T: Fn(u32) -> u32>  
{  calculation: T,  value: HashMap<u32, Option<u32>>,  
}  impl<T> ForFun<T>  
where  T: Fn(u32) -> u32  
{  fn new(calculation: T) -> ForFun<T> {  ForFun {  calculation,  value: HashMap::new(),  }  }  fn value(&mut self, arg: u32) -> u32 {  match self.value.get(&arg) {  Some(v) => v.unwrap(),  None => {  let v = (self.calculation)(arg);  self.value.insert(arg, Some(v));  v  }  }  }  
}

这个例子中的缓存器只能接受同样的参数类型和返回值类型。如果想让闭包的参数类型和返回值类型不一样,就可以引入两个及以上的泛型参数。比如说:

rust">struct ForFun<T, R>  
where  T: Fn(u32) -> R,  
{  calculation: T,  value: Option<R>,  
}

http://www.ppmy.cn/embedded/155075.html

相关文章

【Vue】vue3 video 保存视频进度,每次进入加载上次的视频进度

使用 localStorage 存储每个视频的播放进度在组件加载时恢复上次的播放进度在视频播放过程中实时保存进度在组件卸载前保存最终进度使用 timeupdate 事件来监听视频播放进度的变化 在模板中为视频元素添加事件监听&#xff1a; <videoloopautoplaycontrols:id"video_…

Gitlab搭建npm仓库

由于图片和格式解析问题&#xff0c;为了更好阅读体验可前往 阅读原文 使用gitlab的仓库注册表特性需要版本14.0&#xff0c;如果你的版本比较低&#xff0c;请先根据自己的需求合理升级后再使用 npm私有仓库的搭建方式有很多种&#xff0c;比如使用docker(阅读此篇)&#xff…

Python爬虫学习前传 —— Python从安装到学会一站式服务

早上好啊&#xff0c;大佬们。我们的python基础内容的这一篇终于写好了&#xff0c;啪唧啪唧啪唧…… 说实话&#xff0c;这一篇确实写了很久&#xff0c;一方面是在忙其他几个专栏的内容&#xff0c;再加上生活学业上的事儿&#xff0c;确实精力有限&#xff0c;另一方面&…

C 语言运算符的优先级和结合性

运算符的结合性和优先级 优先级运算符描述结合性1()[]->.函数调用、数组下标、结构体 / 联合体成员通过指针访问、结构体 / 联合体成员访问从左到右2!~ (前缀)-- (前缀) (一元)- (一元)* (间接寻址)& (取地址)sizeof(type)逻辑非、按位取反、前缀自增、前缀自减、一元正…

微服务学习-快速搭建

1. 速通版 1.1. git clone 拉取项目代码&#xff0c;导入 idea 中 git clone icoolkj-microservices-code: 致力于搭建微服务架构平台 1.2. git checkout v1.0.1版本 链接地址&#xff1a;icoolkj-microservices-code 标签 - Gitee.com 2. 项目服务结构 3. 实现重点步骤 …

以太坊(概念与原理)

特点 以太坊是”世界计算机“&#xff0c;开源的、全球分布的计算机基础设施。执行称为智能合约的程序使用区块链来同步和存储系统状态以及名为以太币的加密货币&#xff0c;以计量和约束执行资源成本本质是一个基于交易的状态机以太坊平台使开发人员能够构建具有内置经济功能…

Mac的`~键打出来±§`?解析ANSI、ISO、JIS键盘标准的区别与布局

注&#xff1a;本文由deepseek生成&#xff0c;只进行了基本的校对。 引言 在使用Mac时&#xff0c;你是否遇到过这样的问题&#xff1a;按下~键&#xff0c;却输出了或&#xff1f;这其实是因为你的键盘类型设置与物理键盘布局不匹配。Mac支持多种键盘标准&#xff0c;包括A…

python之二维几何学习笔记

一、概要 资料来源《机械工程师Python编程&#xff1a;入门、实战与进阶》安琪儿索拉奥尔巴塞塔 2024年6月 点和向量&#xff1a;向量的缩放、范数、点乘、叉乘、旋转、平行、垂直、夹角直线和线段&#xff1a;线段中点、离线段最近的点、线段的交点、直线交点、线段的垂直平…