一、闭包
1.1 基本概念
- 闭包(closures)是可以保存进变量或作为参数传递给其他函数的匿名函数;
- 可以在一个地方创建闭包,然后在不同的上下文中执行闭包运算;
- 可从其定义的作用域捕获值;
1.2 使用闭包创建行为的抽象
通过APP生成自定义的健身计划: 重要的不是APP本身的算法,而是仅仅只在需要的时候调用算法且只调用一次。
- 通过调用函数
simulated_expensive_calculation
模拟调用假定的算法; - sleep是算法执行的时间,然后将传入的参数返回回去
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) {
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!(
"Today, run for {} minutes!",
simulated_expensive_calculation(intensity)
);
}
}
}
这里多处调用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!("Today, run for {} minutes!", expensive_result);
}
}
}
- 在
if
分支用到了两次值,在else的else分支用了一次; - 然而在else分支且随机值为3的时候,是不需要调用
simulated_expensive_calculation
函数的,这就造成了浪费; - 使用闭包解决这个问题;
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
- 闭包是一个函数,这个函数的定义可以赋值给一个变量,用
expensive_closure
来表示这个闭包; - 函数的参数放到两个竖线之间,如
| x |
,如果有两个则为| x, y |
; - 最后接一个大括号,写上函数体,这样
simulated_expensive_calculation
就不需要了; - 这里只是定义函数,并没有执行,只有当它遇到小括号时才会执行;
使用闭包修改generate_workout函数
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 {
let result = expensive_closure(intensity);
println!("Today, do {} pushups!", result);
println!("Next, do {} situps!", result);
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!("Today, run for {} minutes!", expensive_closure(intensity));
}
}
}
- 这样就只有在需要的时候才会执行闭包;
if intensity < 25
分支中调用了两次expensive_closure
,可以用闭包的特性用一种新的解决方案;
1.3 闭包类型推断和标注
- 闭包不要求标注参数和返回值的类型;
- 闭包通常很短,只在有限制的上下文工作,编译器可以准确的推断参数和返回值类型;
- 也可以显式的标注参数和返回值类型,如下
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 ; 去掉可选的大括号的闭包
注意:
- 闭包定义会为每个参数和返回值推断一个具体类型。
- 如果两次使用闭包的类型不同,那么会出现错误;
fn main(){
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
- 第一次使用的是String类型,第二次使用i32类型,两者不同,因此会报错。
1.4 使用带有泛型的Fn trait的闭包
- 前面经过优化的
generate_workout
函数依然在intensity < 25
的分支上调用了两次expensive_closure
函数,可以使用惰性求值( memoization 或 lazy evaluation) 的方法进行优化; - 惰性求值是创建一个存放闭包和调用闭包结果的结构体,该结构体只会在需要结果时执行闭包,并会缓存结果值;
- 在结构体中存储闭包,需要指定闭包的类型;
- 闭包有三种方式捕获环境:Fn、FnMut和FnOnce;
名称 | 作用 | 说明 |
---|---|---|
Fn | 从其环境获取不可变 的借用值 | 无需可变访问捕获变量的闭包都实现了Fn |
FnOnce | 从周围作用域捕获的变量且只能被调用一次 | 所有闭包都实现了FnOnce |
FnMut | 获取可变 的借用值所以可以改变其环境 | 没有移动捕获变量的实现了 FnMut |
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
},
}
}
}
- 结构体Cacher有一个泛型T的字段
calculation
; - T的train bound指定了它是一个使用了
Fn
的闭包; - 任何希望储存到Cacher实例的calculation字段的闭包都必须有一个u32参数并必须返回一个u32;
- 字段value是Option<u32> 类型的;在执行闭包之前,value将是None;
- 初次执行闭包的结果储存在value字段的Some成员中,再次执行时直接返回Some成员中的值;
- 闭包的执行结果会调用value方法,如果self.value存在,则直接返回,否则调用self.calculation中储存的闭包,值保存在self.value中,然后返回;
改写generate_workout
函数
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_closure = Cacher::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));
}
}
}
fn main(){
let simulated_user_specified_value = 23;
let simulated_random_number = 8;
generate_workout(
simulated_user_specified_value,
simulated_random_number
);
}
- 先使用
new
生成一个Cacher实例; - 每次使用时用它的
.value()
方法; - 如果
self.value
存在就直接返回值,否则执行自定义的闭包并存储运行结果到self.value中;
1.5 Cacher实现的限制
- Cacher 实例假设对于不同的参数arg,value方法总是会得到相同的返回值;
在main中增加测试函数
#[cfg(test)]
mod tests{
use super::*;
#[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);
}
}
- 这个闭包的意思就是返回的值等于传入的值,然而实际情况并不是这样;
- 传入1时调用Cacher 实例将Some(1) 保存进self.value;
- 然后无论传递什么值调用value,它总是会返回 1;
解决方案: 使用哈希map代替单个值;
- key是传递进去的arg值;
- value则是对应key调用闭包的结果值;
- Cacher只能接收一个u32值并返回一个u32值的闭包;如果需要接收和返回的值属于不同的类型,需要引入两个泛型参数;
1.6 闭包会捕获环境
- 闭包可以访问其作用域内的变量;
- 这会产生额外的内存开销;
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
- x并不是equal_to_x的一个参数;
- equal_to_x与x属于同一个作用域;
- 因此闭包也可能使用变量 x;
1.7 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,编译器会有说明。