24_rust_闭包
闭包
函数式编程的风格特点:
- 函数作为参数
- 函数作为其他函数的返回值
- 函数赋值给变量,之后再执行
rust语言也引入了很多函数式编程的语法特性。
闭包的概念
闭包(closure):可捕获其所在环境的匿名函数。
- 是匿名函数
- 能够保存为变量、作为参数和返回值
- 可在一个地方创建闭包,在另一个上下文中调用闭包
- 可从其定义的作用域捕获值
闭包的定义:
let 闭包名 = |参数1: 可选类型声明, 参数2: 可选类型声明| -> 可选返回值类型 {闭包函数体}; // 整体是一个赋值语句
// 参数类型声明非必要,通常被使用后,编译器能够自动推断
let closure_test = |num: u32| {
println!("calculate test");
thread::sleep(Duration::from_secs(2)); //睡眠2s
num // 函数体返回值
};
闭包的类型推断
和fn定义的函数不同,不强制要求标注参数类型,因为函数是对外暴露的接口,但闭包不是。
- 闭包不要求标注参数和返回值类型
- 闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型
- 也可手动添加类型标注
let closure_test = |num: u32| -> u32 {
println!("calculate test");
thread::sleep(Duration::from_secs(2)); //睡眠2s
num // 函数体返回值
};
函数和闭包的定义语法:
fn test_func(x: u32) -> u32 { x + 1 } //函数
let test1 = |x: u32| -> u32 { x + 1 }; //完整闭包
let test2 = |x| { x + 1 }; //闭包可省略类型标注
let test3 = |x| x + 1 ; //闭包只有一个表达式时还可省略大括号
注:闭包的定义最终只为参数/返回值推断出唯一的具体类型,存在多次使用但类型不同的情况则报错。
let closure1 = |x| x;
let s = closure1(String::from("t")); //本行执行后推断出x的类型是String,并绑定至闭包,后不可再变
let n = closure1(5); // 报错,5是整数类型,但闭包已确定是string类型,类型不匹配
使用泛型参数和Fn Trait存储闭包
创建一个struct,持有闭包及调用结果,只在需要结果时才执行闭包,且能缓存结果,这种模式叫记忆化(memoization)或延迟计算(lazy evaluation)
Fn Trait:
由标准库提供,所有闭包都至少实现了Fn Trait、FnMut Trait、FnOnce Trait三个之一。
例子:希望只调用一次耗时闭包
use std::thread;
use std::time::Duration;
struct Cacher<T> // 泛型T表示闭包的类型
where T: Fn(u32) -> u32, // T的约束是接收u32类型及返回值也是u32类型,类似于函数指针类型定义
{
calc: T,
v: Option<u32>,
}
impl<T> Cacher<T>
where T: Fn(u32) -> u32,
{
fn new(calc: T) -> Cacher<T> {
Cacher {
calc,
v: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.v {
Some(v) => v,
None => {
let v = (self.calc)(arg);
self.v = Some(v);
v
}
}
}
}
fn gen_data(a: u32, rand_num: u32) {
let mut closure1 = Cacher::new(|num| {
println!("calc...");
thread::sleep(Duration::from_secs(2));
num
});
if rand_num < 5 {
println!("test multi {}{}", closure1.value(a), closure1.value(a)); //只会调用一次闭包
} else {
println!("test multi {}{}{}", closure1.value(a), closure1.value(a), closure1.value(a+2)); //只会调用一次闭包
}
}
fn main() {
gen_data(3, 2);
gen_data(5, 6);
}
不过上面代码可看出,多次调用value传入不同参数时,获得的值一样的,因为第一次计算后v有值了就不再调用闭包了。这种情况可采用HashMap代替当个值的方式解决,当key是传入的参数,value是执行闭包的结果,当key不存在时就会调用一次闭包。
闭包捕获上小文
就是闭包能够捕获其所在上下文的变量,但函数不行。
fn main() {
let x = 5;
let closure_test = |z: u32| z == x; // 能够捕获当前作用域内的x变量
fn test_func(z: u32) { // 试图定义一个函数捕获
z == x // 编译报错can't capture dynamic environment in a fn item
}
}
捕获环境信息时会产生内存开销。
闭包从所在环境捕获值的方式
与函数获得参数的三种方式一样:
- 1 取得所有权:FnOnce
- 2 可变借用:FnMut
- 3 不可变借用:Fn
创建闭包时,通过闭包对环境值的使用,rust推断出具体使用哪个trait:
- 所有的闭包都实现了FnOnce
- 没有移动捕获变量的实现了FnMut
- 无需可变访问捕获变量的闭包实现了Fn
move关键字
在参数列表前使用move关键字,可强制闭包取得所使用的环境值的所有权,当闭包传递新线程以移动数据使其归新线程所有事,常用此技术。
fn main() {
let x = 5;
let closure_test = move |z: u32| z == x;
println!("move after{:?}", x);
println!("{:?}", closure_test(3));
}
在教程中会报borrow of moved value:x,variable moved due to use in closure的错误,但本人实际测试未抱错,能正常运行。