Rust | 04. 迭代器与闭包、代码发布、智能指针、多线程
250829 闭包
问题引入
-
Rust在设计过程中也受函数式编程的影响(将函数作为变量、参数或返回值的思想),闭包就是其产物之一
-
简单而言,闭包是
可以捕获其所在环境的匿名函数 ,主要有如下几个特征:是匿名函数,可以保存为变量或作为参数,可以在一个地方创建另一个上下文中调用,可以从其定义的作用域捕获值 -
看如下例子,要求程序根据用户的健康参数动态地生成运动推荐,在该实现中,假定计算过程需要几秒钟时间才能完成,程序当下的目标是不让用户发生不必要的等待,
仅在必要时调用该算法一次 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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: i32, random_number: u32) { // let expensive_result = simulated_expensive_calculation(intensity); 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) ); } } }
-
针对该代码,最常见的处理思路是用一个变量,将这个函数在最开始运行一次,并将结果保存起来。但这样同样会有问题,就是说,有一种情况下,程序可以根本不执行该程序,这样做并不能完美达成仅必要时执行的目的。如果有一种方法,先把函数定义好,但不执行,只有等到真正需要这个结果时才执行,就可以解决这个问题。
-
如果可以有这样一种结构体,其可以持有一个用户输入的、可以变化的函数,并且当该函数的调用一次后可以缓存其结果,这样就可以做到只在需要结果时,执行一次并缓存结果,下次使用只需要判断这个值是否存在就可以。这个模式叫做记忆化(memoization)或延迟计算(lazy evaluation)。这种解决思路就需要用到闭包可以作为参数,输入并保存在结构体中的特点。
闭包的定义
-
闭包的定义方法如下
let res = |parameters|{function body};
,这里等号左边为函数的执行结果,等号的右边为一个匿名函数,参数用|
包裹,但是不强制要求标注参数和返回值的类型,其通常很短,在狭小的上下文中工作,编译器通常能推断出类型。此外,其存在与变量中,甚至不需要命名,就不会暴露给外部用户。函数体的写法和其他地方是一样的。在当下函数的定义阶段,其并不会执行,res
值为暂时为函数的定义,其只有在遇到(arguements)
时才会执行。1 2 3 4
fn add_1 (x: u32) -> u32 { x+1 } let add_2=|x :u32| -> u32 { x+1 }; let add_3=|x| { x+1 }; let add_4=|x| x+1 ;
-
需要注意的时,闭包的定义最终只会为参数/返回值推断出唯一具体的类型。如果第3、4行都不存在,即闭包未被调用时,函数会报错,因为编译器无法推断出其类型;当程序读到第3行时,闭包的参数类型就被界定为字符串类型,第4行会报错。
1 2 3 4 5
fn main() { let example_closure = |x| x; let s = example_closure(String::from("hello")); // let n = example_closure(5); //会报错 }
-
另外,对结构体的定义需要知道所有字段的类型,也同样需要指明闭包的类型。每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样,其类型也是不一样的。因此,为了能够在结构体中存储闭包,就需要使用到泛型以及Trait Bound。在标准库中提供了一些
Fn Trait
,所有的闭包都需要至少实现其中的一种(Fn
、FnMut
、FnOnce
)。 -
上面问题的代码可以做如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
use std::thread; use std::time::Duration; 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: i32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity }); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result(intensity) ); println!( "Next, do {} situps!", expensive_result(intensity) ) } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result(intensity) ); } } }
-
代码在修改后已经可以达成最初的目的了,但是仍旧存在问题。当前的缓存器(Cacher)实例只使用第一次调用时的参数,并只得到一个结果,并且输入的类型和输出的类型需要一致。可以这样解决,
- 将结果value定义为一个HashMap,其根据输入的参数来获取结果,当结果不存在时再进行计算
- 输入和输出也可以使用两个不同的泛型表示方法来实现拓展
捕获上下文环境
-
闭包的另一个特点是其可以访问其定义位置作用域内的变量,而其他常规函数则没有这个能力。看如下例子,当需要判断环境中某个特定值作为函数定义的一部分时,上下文环境中的变量是不可以进入到函数定义作用域内的。需要注意的是,闭包在捕获周围变量时,是会有额外内存开销的,这个也跟普通函数不同。
1 2 3 4 5 6 7
fn main() { let x = 4; let equal_to_x = |z| z==x; // fn equal_to_?(z: i32) -> bool { z==x? } let y = 4; assert!(equal_to_x(y)); }
-
闭包从周围环境中捕获值的三种方式:
- 取得所有权
FnOnce
:即闭包可以获得定义处周围变量的所有权,并将其消耗掉 - 可变借用
FnMut
:其从环境中借用值,且可修改 - 不可变借用
Fn
:从环境中借用值,但不可修改
- 取得所有权
-
在创建闭包时,通过闭包对环境值的使用,Rust就能推断出具体是使用了哪个Trait;具体而言,所有必要都至少会被调用一次,因而一定都实现了
FnOnce
,余下的,没有移动捕获变量的实现了FnMut
,无需可变访问捕获变量的闭包则实现了Fn
。其三者存在包含关系如下:(FnOnce(FnMut(Fn)))
(不理解先记下来)
move关键字
-
在参数列表前使用
move
关键字,可以强制闭包取得它所使用的环境值的所有权。当将闭包传递给新线程以移动数据使其归新线程所有时,这个技术就很有用。1 2 3 4 5 6 7
fn main() { let x = vex![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)); }
-
当需要指定
Fn
Trait Bound时,最好先使用Fn
,然后编译器会告诉你能不能行,或者需要其他类型的