闭包(closures)
闭包:可以捕获其所在环境的匿名函数 闭包:
-
是匿名函数 -
可以保存为变量,作为参数 -
可以在一个地方创建闭包,然后再另一个上下文中调用闭包来完成运算 -
可从其定义的作用域捕获值
闭包的定义
闭包的定义格式
let var = |变量名:变量类型| -> 返回值类型{
函数体;
返回值
}
-
如果变量类型可以自动推断出,可以省略 -
返回值类型没有或可以自动推断,可以省略 -
函数体只有一行,括号可以省略
闭包的类型推断
-
闭包不要求标注参数和返回值类型
-
闭包通常很短小,只在狭小的上下文中工作,编译器通常能推断出类型 -
当然也可以手动添加闭包的类型标注注意:闭包的定义最终只会为参数/返回值推断出唯一具体的类型 -
-
在这个代码中,如果没有下面的两行代码,只有定义闭包,程序会报错,因为无法推断出闭包的参数类型 当有第4行代码后,程序代码就不会报错了,因为根据第4行的代码,可以推断出闭包中的x类型为String 但是有第5行代码,程序还会报错,因为上面已经推断了闭包中x的类型为String,但是第5行的参数类型是number,类型不匹配,所以代码报错
函数和闭包的定义语法区别
使用泛型参数和Fn Trait来存储闭包
为了确保闭包只调用一次,可以使用创建结构体struct,它持有闭包和闭包调用后的结果,在第一次闭包调用后,将结果存储在结构体的变量中,下次可以直接从变量中读取值。避免多次调用闭包
-
只会在需要结果时才执行闭包 -
可缓存结果这个模式通常叫做记忆化(memoization)或延迟计算(lazy evaluation)具体参考下面的实例中的写法
如何让struct持有闭包
-
struct的定义需要知道所有字段的类型 -
需要指定闭包的类型 -
每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样 -
所以需要使用泛型参数和泛型约束(Trait Bound) 具体参考下面的实例中的写法
使用缓存器(Cacher)实现的限制
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实例假定针对不同的参数arg,value方法总会得到同样的值 编写如下的测试代码
#[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);
}
}
测试结果会失败,因为c.value(1);运行后,结构体的value的值变成1,当执行c.value(2)时,value的值不为空,不会重新运行闭包的代码,所以value的值还是1 解决办法:可以使用hashMap代替单个值
-
将arg参数作为hashMap的key -
将value作为arg对应的value
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: HashMap<u32,u32>,
}
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T>{
Cacher{
calculation,
value: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32{
self.value.entry(arg).or_insert((self.calculation)(arg)).clone()
}
}
限制2:只能接收一个u32类型的参数和u32类型的返回值 解决办法:引入多个泛型
理解闭包的用处
例子:生成自定义运动计划的程序
-
算法的逻辑并不是重点,重点是算法中的计算过程需要几秒钟时间 -
目标:不让用户发生不必要的等待 -
仅在必要时调用该算法,而且该算法应该只调用一次 示例代码如下:
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_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)
);
}
}
}
上面的代码中,当intensity小于25时,会打印两句话,并且调用两次耗时的函数,当不小于25,但是随机数不等于3时,也会再调用一次耗时的函数,不符合我们上面的要求,所以需要优化代码
-
对于intensity小于25的情况,可以将函数运行的结果存储在变量中,然后输出变量,就可以保证只调用一次
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之前做的,也就是无论intensity的值是什么,都会运行函数,但是当random_number == 3时,就不需要执行函数,造成运行资源浪费,这就需要使用闭包,先声明闭包,只有在需要的时候才会调用执行
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!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
上面代码在if前定义了一个匿名函数闭包,要求传入一个u32的参数,该匿名函数会返回一个u32类型的值,这里只是声明闭包,并没有调用,实际在后面才会调用,但是这样的代码又出现了一个问题,就是闭包被多次调用了,我们希望的是闭包只调用一次 可以使用struct结构体存储闭包和闭包的结果,需要使用Trait和Trait Bound
struct Cacher<T>
where T: Fn(u32) -> u32
{
calculation: T,
value: Option<u32>,
}
定义了一个结构体,使用泛型参数,约束泛型要求必须是一个Fn,且输入参数为u32类型,返回参数也是u32类型。calculation用于存储闭包,value用于存储闭包的运行结果, 然后为结构体实现关联的构造函数,实现当读取值时,先判断value是否为空,如果为空,则运行闭包获取结果,如果不为空,就直接返回value值
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
}
}
}
}
然后在定义闭包的地方,先创建一个结构体,将闭包的内容传进去,在需要闭包结果的地方,调用结构体的value方法即可
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 x = 4;
let equal_to_x = |z| z==x;
let y =4
assert!(equal_to_x(y));
}
上面的代码可以看出,闭包可以直接访问x变量
如果换成函数定义,就会报错,因为函数不能访问外部的变量
但是这个操作会产生内存开销
闭包从所在环境捕获值的三种方式
与函数获得参数的三种方式一样:
-
取得所有权:FnOnce -
可变借用:FnMut -
不可变借用:Fn 创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个Trait -
所有的闭包都实现了FnOnce -
没有移动捕获变量的实现了FnMut -
无需可变访问捕获变量的闭包实现了Fn 当指定Fn traitBound之一时,首先应该使用Fn,基于闭包体里的情况,rust会提示是否需要FnMut或FnOnce
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));
}
迭代器
什么是迭代器
迭代器模式:对一系列项执行某些任务 迭代器负责:
-
遍历每个项 -
确定序列(遍历)何时完成 Rust的迭代器: -
懒惰的:除非调用消费迭代器的方法,否则迭代器本身没有任何效果
fn main(){
let v1 = vec![1,2,3];
let v1_iter = v1.iter(); //此方法可以生成一个v1的迭代器,但是此时没有使用,所以没有效果
// 使用迭代器
for elem in v1_iter{
println!("{}",elem);
}
}
lterator Trait
所有的迭代器都实现了iterator Trait iterator Trait定义与标准库,定义大致如下:
pub trait iterator{
type item;
fn next(&mut self)-> Option<self::item>;
// 默认实现被省略的方法
}
type item和self::item定义了与此该trait关联的类型
-
实现iterator trait需要你定义一个item类型,它用于next方法的返回类型(迭代器的返回类型)
iterator trait 仅要求实现一个方法:next
-
next:每次返回迭代器中的一项,返回的结果包裹在some里,迭代结束的时候,返回None 可直接在迭代器上调用next方法
#[cfg(test)]
mod tests{
#[test]
fn iterator_demonstration(){
let v1 = vec![1,2,3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(),Some(&1));
assert_eq!(v1_iter.next(),Some(&2));
assert_eq!(v1_iter.next(),Some(&3));
}
}
几个迭代方法
-
iter方法:在不可变引用上创建迭代器 -
into_iter方法:创建的迭代器会获得所有权 -
iter_mut方法:迭代可变的引用
消耗迭代器的方法
在标准库中,iterator trait有一些带默认实现的方法 其中有一些方法会调用next方法
-
实现iterator trait时必须实现next方法的原因之一 调用next的方法叫做”消耗性适配器“ -
因为调用它们会把迭代器耗尽 -
例如:sum方法(就会耗尽迭代器) -
取得迭代器的所有权 -
通过反复调用next,遍历所有元素 -
每次迭代,把当前元素添加到一个总和里,迭代结束,返回总和
产生其他迭代器的方法
-
定义在iterator trait上的另外一些方法叫做”迭代器适配器“ -
把当前的迭代器转换为不同种类的迭代器 -
可以通过链式调用使用多个迭代器适配器来执行复杂的操作,这种调用可读性较高 例如map: -
接收一个闭包,闭包作于每个元素, -
collect方法:消耗性适配器,把结果收集到一个集合类型中
#[cfg(test)]
mod tests{
#[test]
fn iteratior_sum(){
let v1 = vec![1,2,3];
let v2: Vec<i32> = v1.iter().map(|x|x+1).collect();
println!("{:?}",v2);
}
}
示例:输入一个迭代器对象,经过过滤,返回结果还是迭代器
#[derive(PartialEq, Debug)]
struct shoe {
size: u32,
style: String,
}
fn shoes_in_my_size(shoes: Vec<shoe>, shoe_size: u32) -> Vec<shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
fn main() {
let shoes = vec![
shoe {
size: 10,
style: String::from("sneaker"),
},
shoe {
size: 13,
style: String::from("sandal"),
},
shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_my_size(shoes, 10);
println!("{:?}", in_my_size)
}
创建自定义迭代器
使用iterator trait来创建自定义迭代器,主要要求就是实现next方法
struct Counter{
count: u32,
}
impl Counter{
fn new() -> Counter{
Counter{count: 0}
}
}
impl Iterator for Counter{
type Item = u32;
fn next(&mut self) -> Option<Self::Item>{
self.count += 1;
if self.count <= 5{
Some(self.count)
}else{
None
}
}
}
fn main() {
let counter = Counter::new();
for i in counter{
println!("{}", i);
}
}
上面的代码,先定义了一个结构体,包括一个u32类型的数据,然后给结构体添加了构造函数,确保每次创建的结构体对象都是从0开始,然后给结构体实现迭代器trait,当结构体内的值小于等于5时,会返回迭代器的值,并每次运行+1,当大于5时,会返回none。扩展迭代器的方法:要求:
-
第一个迭代器就是现有的1-5,第二个迭代器就是当前的去除第一个元素 -
然后将两个迭代器的元素相乘,产生一个新的迭代器 -
对新的迭代器进行元素过滤,要求能被3整除 -
对剩下的元素求和
fn main(){
let sum = Counter::new().zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
.filter(|x| x % 3 == 0)
.sum::<u32>();
println!("sum: {}", sum);
}
性能笔记:循环VS迭代器
一个测试:把一本小说的内容放在一个string里面,搜索”the“
-
迭代器的版本更快一点 -
-
零开销抽象:使用抽象不会引入格外的运行开销 在rust中,可以尽情使用闭包、迭代器等高层次的抽象,不用去考虑性能
原文始发于微信公众号(宁雪):Rust:闭包和迭代器
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论