rust学习-闭包

背景

模拟健康推荐算法,为前端提供高强度/低强度的训练app

复制代码
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 main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

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 是个算法模块维护的内容,且未来变化较大,所以代码中期待对其只使用一次。

用闭包重构

复制代码
use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    // 闭包的定义以一对竖线(|)开始,在竖线中指定闭包的参数
    // 如果有多于一个参数,可以使用逗号分隔,比如 |param1, param2|
    // 如果闭包体只有一行则大括号是可以省略
    // let 语句意味着 expensive_closure 包含一个匿名函数的 定义
    // 不是调用匿名函数的 返回值
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        // 闭包体的最后一行没有分号(正如函数体一样)
        // 所以闭包体(num)最后一行的值作为调用闭包时的返回值
        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)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

闭包类型推断和标注

闭包不要求像 fn 函数那样在参数和返回值上注明类型

函数需要类型标注是因为是暴露给用户的显式接口的一部分

如果一定要类型标注,也不是不可以

复制代码
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  ;

闭包定义会为每个参数和返回值推断一个具体类型

复制代码
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
// 编译器推断 x 和此闭包返回值的类型为 String
// 这些类型被锁定进闭包 example_closure 中
// 如果尝试对同一闭包使用不同类型则会得到类型错误
// 如下编译则报错
// let n = example_closure(5);

使用带有泛型和 Fn trait 的闭包

解决在一个上下文中,闭包被重复计算的问题

方法一:

创建一个存放闭包和调用闭包结果的变量(不同的上下文可能变量会很多)

方法二:

创建一个存放闭包和调用闭包结果的结构体

该结构体只会在需要结果时执行闭包,并会缓存结果值,即缓存

lazy evaluation (惰性求值)

复制代码
// 定义一个 Cacher 结构体来在 calculation 中存放闭包
// 在 value 中存放 Option 值
// 结构体中存储闭包类型:闭包有一个 u32 的参数并返回一个 u32
// 这样所指定的 trait bound 就是 Fn(u32) -> u32
// 所有的闭包都实现了 trait Fn、FnMut 或 FnOnce 中的一个,这里使用 trait Fn
//
// 注意:函数也都实现了这三个 Fn trait
// 如果不需要捕获环境中的值,则可以使用实现了 Fn trait 的函数而不是闭包
//
// 在执行闭包之前,value 将是 None
// 如果使用 Cacher 的代码请求闭包的结果,这时会执行闭包并将结果储存在 value 字段的 Some 成员中。接着如果代码再次请求闭包的结果,这时不再执行闭包,而是会返回存放在 Some 成员中的结果
use std::thread;
use std::time::Duration;

struct Cacher<T>
    where T: Fn(u32) -> u32
{
    // Cacher 结构体的字段是私有的,代表着仅由Cacher的方法管理它们
    calculation: T,
    value: Option<u32>,
}

impl<T> Cacher<T>
    where T: Fn(u32) -> u32
{
    // Cacher::new 获取一个泛型参数 T,和结构体有着相同的trait bound
    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); // 将结果存储到Some中
                v
            },
        }
    }
}

fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} situps!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

该cacher的缺点

(1)Cacher 实例假设对于 value 方法的任何 arg 参数值总是会返回相同值

复制代码
#[test]
fn call_with_different_values() {
    let mut c = Cacher::new(|a| a);

    let v1 = c.value(1); // 此时缓存中的值是1
    let v2 = c.value(2); // 此时缓存中的值还是1

    assert_eq!(v2, 2);
}

解决办法:Cacher 存放哈希 map 而不是单独一个值

哈希 map 的 key 将是传递进来的 arg 值

哈希 map 的 value 则是对应 key 调用闭包的结果值

value 函数会在哈希 map 中寻找 arg,如果找到的话就返回其对应的值。

如果不存在,Cacher 会调用闭包并将结果值保存在哈希 map 对应 arg 值的位置。

(2)不够泛型

它的应用被限制为只接受获取一个 u32 值并返回一个 u32 值的闭包

如果需要能够缓存一个获取字符串 slice 并返回 usize 值的闭包的结果呢?

闭包捕获其环境

闭包还有另一个函数所没有的功能:

他们可以捕获其环境并访问其被定义的作用域的变量

当闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用。

这会使用内存并产生额外的开销

复制代码
fn main() {
    let x = 4;
    // 闭包中直接使用变量x,因为它与 equal_to_x 定义于相同的作用域
    // 函数则不行
    let equal_to_x = |z| z == x;
    let y = 4;
    assert!(equal_to_x(y));
}

捕获环境的三种方式:

闭包周围的作用域被称为其环境,environment

对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用

创建一个闭包时,Rust 根据其如何使用环境中变量来推断如何引用环境。

FnOnce

为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。

其名称的 Once 部分代表了闭包不能多次获取相同变量的所有权,所以它只能被调用一次。

FnMut

获取可变的借用值,可以改变其环境

Fn

从其环境获取不可变的借用值

复制代码
fn main() {
    // 整型可以被拷贝而不是移动,所以这里采用vec
    let x = vec![1, 2, 3];
    
    // 强制闭包获取其使用的环境值的所有权
    // 闭包获取了 x 的所有权
    let equal_to_x = move |z| z == x;
    // 这里x无法打印,编译报错
    // println!("can't use x here: {:?}", x);

    let y = vec![1, 2, 3];

    assert!(equal_to_x(y));
}
相关推荐
码智社11 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海11 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖12 小时前
JDK 26 新特性详解
java·开发语言
马优晨12 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a11177613 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao14 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
doiito14 小时前
【AI 应用】从“外国人味”到地道中文:kokoroi-rs v0.1.2 架构升级深度解析
ai·rust·系统设计
人邮异步社区14 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大14 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai15 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang