赞
踩
函数式编程风格通常包括将函数作为另一个函数的参数、返回值,将函数作为值赋值给变量,以供后续执行
本章中我们将会介绍以下内容:
闭包:一个可以存储在变量里的类似函数的数据结构
迭代器:一种处理元素序列的方式
如何使用这些功能来改进第十二章的I/O项目
这两个功能的性能(剧透警告:它们的速度超乎你的想象)
我40米的大刀已经饥渴难耐了,让我们攻下这一章!
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 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!
使用函数重构
现在让我们来重构一下程序
我们在上面多次使用了函数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: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
这是因为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));
}
为了展示闭包作为函数参数时捕获其环境的作用,我们接下来学习迭代器
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。