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

打印 上一主题 下一主题

主题 912|帖子 912|积分 2736

13.3.0. 写在正文之前

Rust语言在筹划过程中收到了很多语言的启发,而函数式编程对Rust产生了非常明显的影响。函数式编程通常包括通过将函数作为值通报给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。
在本章中,我们会讨论 Rust 的一些特性,这些特性与许多语言中通常称为函数式的特性相似:


  • 闭包(本文)
  • 迭代器
  • 利用闭包和迭代器改进I/O项目
  • 闭包和迭代器的性能
喜好的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

13.3.1. 回顾

还记得在 13.1 中的例子吗:
做一个程序,根据人的身材指数等因素生成自定义的活动筹划。这个程序的算法逻辑并不是重点,重点是算法在计算过程中会泯灭几秒的时间。我们的目的是不让用户发生不必要的等待。具体来说就是仅在必要的时间才调用该算法,而且只调用一次。
其时我们修改代码为:
  1. use std::thread;  
  2. use std::time::Duration;  
  3.   
  4. fn main() {  
  5.     let simulated_user_specified_value = 10;  
  6.     let simulated_random_number = 7;  
  7.   
  8.     generate_workout(  
  9.         simulated_user_specified_value,  
  10.         simulated_random_number,  
  11.     );  
  12. }  
  13. fn generate_workout(intensity: u32, random_number: u32) {  
  14.     let expensive_closure = |num| {  
  15.         println!("calculating slowly...");  
  16.         thread::sleep(Duration::from_secs(2));  
  17.         num  
  18.     };  
  19.     if intensity < 25 {  
  20.         println!("Today, do {} pushups!", expensive_closure(intensity));  
  21.         println!("Next, do {} situps!", expensive_closure(intensity));
  22.     } else {  
  23.         if random_number == 3 {  
  24.             println!("Take a break today! Remember to stay hydrated!");  
  25.         } else {  
  26.             println!("Today, run for {} minutes!", expensive_closure(intensity));  
  27.         }  
  28.     }  
  29. }
复制代码
但是还存在一个题目:这么写没有解决闭包重复调用的题目。 在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就可以了
知道这些之后就可以修改例子了。起首创建一个布局体:
  1. struct Cache<T: Fn(u32) -> u32>  
  2. {  
  3.     calculation: T,  
  4.     value: Option<u32>,  
  5. }
复制代码


  • 这个布局体有一个泛型参数T,由于它代表的是闭包的类型,它的束缚是Fn trait(在本例中利用Fn就可以了),然后参数和返回值是u32,所以写Fn(u32) -> u32。
  • 闭包所在的字段是calculation,它的类型就是T
  • 要缓存的值在value字段上,其类型是u32,但是要注意的是不清楚这个值是否已经计算出来并缓存在里面了,所以要用Option类型来包裹,也就是Option<u32>。
先在布局体上写一个构造函数用于创建实例:
  1. impl<T: Fn(u32) -> u32> Cache<T> {  
  2.     fn new(calculation: T) -> Cache<T> {  
  3.         Cache {  
  4.             calculation,  
  5.             value: None,  
  6.         }  
  7.     }  
  8. }
复制代码
这么写看着有点乱,可以用where字句重写一下:
  1. impl<T> Cache<T>   
  2. where   
  3. T: Fn(u32) -> u32  
  4. {  
  5.     fn new(calculation: T) -> Cache<T> {  
  6.         Cache {  
  7.             calculation,  
  8.             value: None,  
  9.         }  
  10.     }  
  11. }
复制代码
然后,为了实现value有值就取value下的值,value是None就计算的功能,再写一个函数:
  1. fn value(&mut self, arg: u32) -> u32 {  
  2.     match self.value {  
  3.         Some(v) => v,  
  4.         None => {  
  5.             let v = (self.calculation)(arg);  
  6.             self.value = Some(v);  
  7.             v  
  8.         }  
  9.     }  
  10. }
复制代码
假如实例的value字段有值就返回这个值,没有值就计算出这个值,存储在value字段里再返回。
这部分写好之后,就该把generate_workout的写法改一下,转为利用cache布局体:
  1. fn generate_workout(intensity: u32, random_number: u32) {  
  2.     let mut expensive_closure = Cache::new(|num|{  
  3.         println!("calculating slowly...");  
  4.         thread::sleep(Duration::from_secs(2));  
  5.         num  
  6.     });  
  7.     if intensity < 25 {  
  8.         println!("Today, do {} pushups!", expensive_closure.value(intensity));  
  9.         println!("Next, do {} situps!", expensive_closure.value(intensity));  
  10.     } else {  
  11.         if random_number == 3 {  
  12.             println!("Take a break today! Remember to stay hydrated!");  
  13.         } else {  
  14.             println!("Today, run for {} minutes!", expensive_closure.value(intensity));  
  15.         }  
  16.     }  
  17. }
复制代码


  • 把expensive_closure作为Cache布局体的实例,利用new函数把闭包传进去。这里把expensive_closure加上mut设为可变函数是由于后文调用时大概会改变value这个字段的值。
  • 下文所有要利用值的操作都利用value方法来获取。
13.3.4. 利用缓存器实现的限定

这里的Cache字段就是缓存器,用于缓存某个值,但这么写是有限定的。
我把Cache的声明和其方法的代码贴在这里:
  1. struct Cache<T: Fn(u32) -> u32>  
  2. {  
  3.     calculation: T,  
  4.     value: Option<u32>,  
  5. }
  6.     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就会得到最开始的计算的这个值,岂论传进去的参数是什么。
这么说大概有点模糊,那来看个例子:
  1. fn call_with_different_values(){
  2.         let mut c = Cache::new(|a| a);
  3.         let v1 = c.value(1);
  4.         let v2 = c.value(2);
  5. }
复制代码


  • c是Cache的一个实例,传进去了一个闭包。
  • 在let v1 = c.value(1);这一行时原本c的value字段没有值,这时间传进去个1,value字段就变成Some(1)了(value字段是Option类型)
  • 在let v2 = c.value(2);这一行时由于value字段原本有值,所以会直接取value字段的1赋给v2,纵然这行的value方法的参数与上一行不一样。
假如不想要这样,就得利用HashMap来代替单个的值,把HashMap的key作为value方法传进去的参数args;而值就作为执行闭包的效果。比如说:
  1. struct ForFun<T: Fn(u32) -> u32>  
  2. {  
  3.     calculation: T,  
  4.     value: HashMap<u32, Option<u32>>,  
  5. }  
  6.   
  7. impl<T> ForFun<T>  
  8. where  
  9.     T: Fn(u32) -> u32  
  10. {  
  11.     fn new(calculation: T) -> ForFun<T> {  
  12.         ForFun {  
  13.             calculation,  
  14.             value: HashMap::new(),  
  15.         }  
  16.     }  
  17.       
  18.     fn value(&mut self, arg: u32) -> u32 {  
  19.         match self.value.get(&arg) {  
  20.             Some(v) => v.unwrap(),  
  21.             None => {  
  22.                 let v = (self.calculation)(arg);  
  23.                 self.value.insert(arg, Some(v));  
  24.                 v  
  25.             }  
  26.         }  
  27.     }  
  28. }
复制代码
这个例子中的缓存器只能接受同样的参数类型和返回值类型。假如想让闭包的参数类型和返回值类型不一样,就可以引入两个及以上的泛型参数。比如说:
  1. struct ForFun<T, R>  
  2. where  
  3.     T: Fn(u32) -> R,  
  4. {  
  5.     calculation: T,  
  6.     value: Option<R>,  
  7. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

尚未崩坏

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表