1、使用闭包创建抽象行为
什么是闭包(closure)
闭包:可以捕获其所在环境的匿名函数。
闭包:
是匿名函数
保存为变量、作为参数
可在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算
可从其定义的作用域捕获值
我们现在写一个程序:生成自定义运动计划的程序
算法的逻辑并不是重点,重点是算法中的计算过程需要几秒钟时间。
目标:不让用户发生不必要的等待
仅在必要时调用该算法
只调用一次
创建一个项目
cargo new closure
使用vs code 打开
cd closure
code .
在main函数中编写代码
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 simulated_expensive_calcalation(intensity:u32) ->u32{
println!("calculating slowly...");
thread::sleep(Duration::from_micros(2));
intensity
}
fn generate_workout(intensity:u32,random_number:u32) {
if intensity < 25 {
println!(
"Today,do {} pushups!",
simulated_expensive_calcalation(intensity)
);
println!(
"Next,do {} situps!",
simulated_expensive_calcalation(intensity)
);
} else {
if random_number == 3 {
println!("random_number == 3")
} else {
println!("random_number != 3,{}",simulated_expensive_calcalation(intensity))
}
}
}
上诉代码中我们函数的调用我们需要调用好几次,这样就增加了系统的开销
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 simulated_expensive_calcalation(intensity:u32) ->u32{
println!("calculating slowly...");
thread::sleep(Duration::from_micros(2));
intensity
}
fn generate_workout(intensity:u32,random_number:u32) {
let expensive_result = simulated_expensive_calcalation(intensity);
if intensity < 25 {
println!(
"Today,do {} pushups!",
expensive_result
);
println!(
"Next,do {} situps!",
expensive_result
);
} else {
if random_number == 3 {
println!("random_number == 3")
} else {
println!("random_number != 3,{}",expensive_result)
}
}
}
从上面两次的结果中可以看出第二次只调用了一次函数
但是上面的代码还是有问题的,不如说在random_number等于3的时候,我们是不需要调用那个复杂的函数的。
我们真正需要的是函数定义放在一个地方,只有我们真正需要的时候在执行,这才是闭包真正的用武之地
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 simulated_expensive_calcalation(intensity:u32) ->u32{
println!("calculating slowly...");
thread::sleep(Duration::from_micros(2));
intensity
}
fn generate_workout(intensity:u32,random_number:u32) {
let expensive_closure = |num:u32| {
println!("calculating slowly...");
thread::sleep(Duration::from_micros(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!("random_number == 3")
} else {
println!("random_number != 3,{}",expensive_closure(intensity))
}
}
}
上面的代码就是使用了闭包,但是大家很快就会发现,它和函数没有什么区别,函数还是会有调用两次的问题?那么怎么解决呢?我们继续看
2、闭包类型推断和标注
闭包的类型推断
闭包不要求标注参数和返回值的类型
闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
可以手动添加类型标注
let expensive_closure = |num:u32|->u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_micros(2));
num
};
函数和闭包的定义语法
闭包的类型推断
注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型
fn main() {
let example = |x|x;
let one = example(1);
let two = example(String::from("1"));
}
这里的two这个闭包的调用就会报错,因为闭包在第一次调用的时候就会推断出参数的类型和返回值的类型,后面的调用如果参数类型不相同就会报错
3、使用泛型参数和 Fn Trait 来存储闭包
之前我们的那个运动计划的程序有一个问题,就是函数闭包会调用两次的问题。我们提出的解决方案是用一个本地的变量来保存闭包的值,但是这种解决方案也可能造成大量的代码重复。现在我们使用另外一种解决方案来完成
创建一个struct,它持有闭包及其调用结果。
只会在需要结果时才执行该闭包
可缓存结果
这个模式通常叫做记忆化(memoization)或延迟计算(lazy evaluation)
如何用struct持有闭包
struct的定义需要知道所有字段的类型
需要指明闭包的类型
每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样。
所以需要使用:泛型和Trait Bound
Fn Trait
Fn traits由标准库提供
所有的闭包都至少实现了以下trait 之一:
Fn
FnMut
FnOnce
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);
}
struct Cacher<T>
where T:Fn(u32) -> u32
{
calculation:T,
value:Option<u32>
}
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
}
}
}
}
fn generate_workout(intensity:u32,random_number:u32) {
let mut expensive_closure = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_micros(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!("random_number == 3")
} else {
println!("random_number != 3,{}",expensive_closure.value(intensity))
}
}
}
上面的代码我们通过了一个struct来实现了闭包的一次调用,当多次发生调用时,使用的是缓存的值
使用缓存器(Cacher)实现的限制
1. Cacher实例假定针对不同的参数 arg,value方法总会得到同样的值。
#[cfg(test)]
mod tests {
#[test]
fn call_with_different_values() {
let mut c = super::Cacher::new(|a|a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2,2);
}
}
运行cargo test后发现测试不通过
那么如何解决这个问题呢?
可以使用HashMap 代替单个值
key: arg参数
value:执行闭包的结果
只能接收一个u32类型的参数和u32类型的返回值
4、使用闭包捕获环境
闭包可以捕获他们所在的环境
闭包可以访问定义它的作用域内的变量,而普通函数则不能。
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y))
}
会产生内存开销。
闭包从所在环境捕获值的方式
与函数获得参数的三种方式一样:
1.取得所有权:FnOnce
2.可变借用:FnMut
3.不可变借用:Fn
创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个trait:
所有的闭包都实现了FnOnce
没有移动捕获变量的实现了FnMut
无需可变访问捕获变量的闭包实现了Fn
move关键字
在参数列表前使用move关键字,可以强制闭包取得它所使用的环境值的所有权
当将闭包传递给新线程以移动数据使其归新线程所有时,此技术最为有用。
fn main() {
let x = vec![1,2,3];
let equal_to_x = move |z| z == x;
println!("can't use x here :{:?}",x);
let y = vec![1,2,3];
assert!(equal_to_x(y))
}
这里x的所有权就会移动到闭包里面
当我们想再次使用的时候就会报错。
当指定Fn trait bound 之一时,首先用Fn,基于闭包体里的情况,如果需要FnOnce或FnMut,编译器会再告诉你。