18.闭包与迭代器(上)

一、闭包

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实现的限制

  1. 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调用闭包的结果值;
  1. 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,编译器会有说明。
  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贱贱的剑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值