rust 闭包

文章目录

      • 闭包
        • 使用闭包来简化代码
          • 传统函数实现
        • 闭包实现
        • 闭包的类型推导
        • 结构体中的闭包
        • 捕获作用域中的值
        • 三种 Fn 特征
        • 闭包作为函数返回值

闭包

闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数,不同于函数的是,它允许捕获调用者作用域中的值,例如:

fn main() {let x = 1;let sum = |y| x + y;assert_eq!(3, sum(2));
}

上面的代码展示了非常简单的闭包 sum,它拥有一个入参 y,同时捕获了作用域中的 x 的值,因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和:3。

可以看到 sum 非常符合闭包的定义:可以赋值给变量,允许捕获调用者作用域中的值。

使用闭包来简化代码
传统函数实现

想象一下,我们要进行健身,用代码怎么实现,这里是我的想法:

use std::thread;
use std::time::Duration;// 开始健身,好累,我得发出声音:muuuu...
fn muuuuu(intensity: u32) -> u32 {println!("muuuu.....");thread::sleep(Duration::from_secs(2));intensity
}fn workout(intensity: u32, random_number: u32) {if intensity < 25 {println!("今天活力满满,先做 {} 个俯卧撑!",muuuuu(intensity));println!("旁边有妹子在看,俯卧撑太low,再来 {} 组卧推!",muuuuu(intensity));} else if random_number == 3 {println!("昨天练过度了,今天还是休息下吧!");} else {println!("昨天练过度了,今天干干有氧,跑步 {} 分钟!",muuuuu(intensity));}
}fn main() {// 强度let intensity = 10;// 随机值用来决定某个选择let random_number = 7;// 开始健身workout(intensity, random_number);
}

可以看到,在健身时我们根据想要的强度来调整具体的动作,然后调用 muuuuu 函数来开始健身。这个程序本身很简单,没啥好说的,但是假如未来不用 muuuuu 函数了,是不是得把所有 muuuuu 都替换成,比如说 woooo ?如果 muuuuu 出现了几十次,那意味着我们要修改几十处地方。

闭包实现
use std::thread;
use std::time::Duration;fn workout(intensity: u32, random_number: u32) {let action = || {println!("muuuu.....");thread::sleep(Duration::from_secs(2));intensity};if intensity < 25 {println!("今天活力满满,先做 {} 个俯卧撑!",action());println!("旁边有妹子在看,俯卧撑太low,再来 {} 组卧推!",action());} else if random_number == 3 {println!("昨天练过度了,今天还是休息下吧!");} else {println!("昨天练过度了,今天干干有氧,跑步 {} 分钟!",action());}
}fn main() {// 动作次数let intensity = 10;// 随机值用来决定某个选择let random_number = 7;
FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:// 开始健身workout(intensity, random_number);
}

在上面代码中,无论你要修改什么,只要修改闭包 action 的实现即可,其它地方只负责调用,完美解决了我们的问题!

Rust 闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |parm1| 的形式进行声明,如果是多个参数就 |param1, param2,…|, 下面给出闭包的形式定义:

|param1, param2,...| {语句1;语句2;返回表达式
}
闭包的类型推导

与函数相反,闭包并不会作为 API 对外提供,因此它可以享受编译器的类型推导能力,无需标注参数和返回值的类型。
下面展示了同一个功能的函数和闭包实现形式:

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"));
let n = example_closure(5);

首先,在 s 中,编译器为 x 推导出类型 String,但是紧接着 n 试图用 5 这个整型去调用闭包,跟编译器之前推导的 String 类型不符,因此报错:

error[E0308]: mismatched types--> src/main.rs:5:29|
5 |     let n = example_closure(5);|                             ^|                             ||                             expected struct `String`, found integer // 期待String类型,却发现一个整数|                             help: try using a conversion method: `5.to_string()`
结构体中的闭包

假设我们要实现一个简易缓存,功能是获取一个值,然后将其缓存起来,那么可以这样设计:

  • 一个闭包用于获取值
  • 一个变量,用于存储该值
    可以使用结构体来代表缓存对象,最终设计如下:
struct Cacher<T>
whereT: Fn(u32) -> u32,
{query: T,value: Option<u32>,
}

特征 Fn(u32) -> u32 从表面来看,就对闭包形式进行了显而易见的限制:该闭包拥有一个u32类型的参数,同时返回一个u32类型的值。

接着,为缓存实现方法:

impl<T> Cacher<T>
whereT: Fn(u32) -> u32,
{fn new(query: T) -> Cacher<T> {Cacher {query,value: None,}}// 先查询缓存值 `self.value`,若不存在,则调用 `query` 加载fn value(&mut self, arg: u32) -> u32 {match self.value {Some(v) => v,None => {let v = (self.query)(arg);self.value = Some(v);v}}}
}

上面的缓存有一个很大的问题:只支持 u32 类型的值,若我们想要缓存 &str 类型,显然就行不通了,因此需要将 u32 替换成泛型 E,该练习就留给读者自己完成,具体代码可以参考这里

捕获作用域中的值

在之前代码中,我们一直在用闭包的匿名函数特性(赋值给变量),然而闭包还拥有一项函数所不具备的特性:捕获作用域中的值。

fn main() {let x = 4;let equal_to_x = |z| z == x;let y = 4;assert!(equal_to_x(y));
}

上面代码中,x 并不是闭包 equal_to_x 的参数,但是它依然可以去使用 x,因为 equal_to_x 在 x 的作用域范围内。

对于函数来说,就算你把函数定义在 main 函数体中,它也不能访问 x:

fn main() {let x = 4;fn equal_to_x(z: i32) -> bool {z == x}let y = 4;assert!(equal_to_x(y));
}

报错如下:

error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境--> src/main.rs:5:14|
5 |         z == x|              ^|= help: use the `|| { ... }` closure form instead // 使用
三种 Fn 特征
  1. FnOnce,该类型的闭包会拿走被捕获变量的所有权。Once 顾名思义,说明该闭包只能运行一次:
fn fn_once<F>(func: F)
whereF: FnOnce(usize) -> bool,
{println!("{}", func(3));println!("{}", func(4));
}fn main() {let x = vec![1, 2, 3];fn_once(|z|{z == x.len()})
}
  1. FnMut,它以可变借用的方式捕获了环境中的值,因此可以修改该值:
fn main() {let mut s = String::new();let update_string =  |str| s.push_str(str);update_string("hello");println!("{:?}",s);
}
  1. Fn 特征,它以不可变借用的方式捕获环境中的值 让我们把上面的代码中 exec 的 F 泛型参数类型修改为 Fn(&'a str):
fn main() {let mut s = String::new();let update_string =  |str| s.push_str(str);exec(update_string);println!("{:?}",s);
}fn exec<'a, F: Fn(&'a str)>(mut f: F)  {f("hello")
}
闭包作为函数返回值

但是如果要使用闭包作为函数返回值,该如何做?

先来看一段代码:

fn factory() -> Fn(i32) -> i32 {let num = 5;|x| x + num
}let f = factory();let answer = f(1);
assert_eq!(6, answer);

上面这段代码看起来还是蛮正常的,用 Fn(i32) -> i32 特征来代表 |x| x + num,非常合理嘛,肯定可以编译通过, 可惜理想总是难以照进现实,编译器给我们报了一大堆错误,先挑几个重点来看看:

fn factory<T>() -> Fn(i32) -> i32 {|                    ^^^^^^^^^^^^^^ doesn't have a size known at compile-time // 该类型在编译器没有固定的大小

Rust 要求函数的参数和返回类型,必须有固定的内存大小,例如 i32 就是 4 个字节,引用类型是 8 个字节,总之,绝大部分类型都有固定的大小,但是不包括特征,因为特征类似接口,对于编译器来说,无法知道它后面藏的真实类型是什么,因为也无法得知具体的大小。

同样,我们也无法知道闭包的具体类型,该怎么办呢?再看看报错提示:

help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/main.rs:11:5: 11:21]`, which implements `Fn(i32) -> i32`|
8 | fn factory<T>() -> impl Fn(i32) -> i32 {

嗯,编译器提示我们加一个 impl 关键字,哦,这样一说,读者可能就想起来了,impl Trait 可以用来返回一个实现了指定特征的类型,那么这里 impl Fn(i32) -> i32 的返回值形式,说明我们要返回一个闭包类型,它实现了 Fn(i32) -> i32 特征。

完美解决,但是,在特征那一章,我们提到过,impl Trait 的返回方式有一个非常大的局限,就是你只能返回同样的类型,例如:

fn factory(x:i32) -> impl Fn(i32) -> i32 {let num = 5;if x > 1{move |x| x + num} else {move |x| x - num}
}

运行后,编译器报错:

error[E0308]: `if` and `else` have incompatible types--> src/main.rs:15:9|
12 | /     if x > 1{
13 | |         move |x| x + num| |         ---------------- expected because of this
14 | |     } else {
15 | |         move |x| x - num| |         ^^^^^^^^^^^^^^^^ expected closure, found a different closure
16 | |     }| |_____- `if` and `else` have incompatible types|

嗯,提示很清晰:if 和 else 分支中返回了不同的闭包类型,这就很奇怪了,明明这两个闭包长的一样的,好在细心的读者应该回想起来,本章节前面咱们有提到:就算签名一样的闭包,类型也是不同的,因此在这种情况下,就无法再使用 impl Trait 的方式去返回闭包。

只需要用 Box 的方式即可实现:

fn factory(x:i32) -> Box<dyn Fn(i32) -> i32> {let num = 5;if x > 1{Box::new(move |x| x + num)} else {Box::new(move |x| x - num)}
}

至此,闭包作为函数返回值就已完美解决,若以后你再遇到报错时,一定要仔细阅读编译器的提示,很多时候,转角都能遇到爱。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/138949.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【m98】webrtc vs2017构建带符号的debug库

调试有符号 调试 无符号 试试exe不输出到独立的文件? -】 直接输出到sln下面

Semantic Kernel 学习笔记1

1. 挂代理跑通openai API 2. 无需魔法跑通Azure API 下载Semantic Kernel的github代码包到本地&#xff0c;主要用于方便学习python->notebooks文件夹中的内容。 1. Openai API&#xff1a;根据上述文件夹中的.env.example示例创建.env文件&#xff0c;需要填写下方两个内…

aws亚马逊云:置以使用 Amazon EC2!!!

完成本部分中的任务&#xff0c;以便为首次启动 Amazon EC2 实例进行设置&#xff1a; 注册一个 AWS 账户 创建管理用户 创建密钥对 创建安全组 完成后&#xff0c;您将准备好学习 Amazon EC2 入门教程。 注册一个 AWS 账户 如果您还没有 AWS 账户&#xff0c;请完成以下…

【神经网络】GAN:生成对抗网络

GAN&#xff1a;生成对抗网络 Generator&#xff08;生成器&#xff09;概念 和传统的神经网络不同&#xff0c;Generator除了接受x的输入之外&#xff0c;还会接受一个简单的分布作为z进行输入&#xff0c;从而使得网络的输出也是一个复杂的分布 为什么输出需要时一个分布呢…

2-CentOS7.9下安装docker

默认情况下,CentOS7.9下有两种方法可以安装docker,分别是在线安装docker和离线安装docker(伪离线,最后还是需要网络支持) 1.环境信息 HostNameIPAddressOS VersionDocker VersionNotecentos79172.20.10.12CentOS Linux release 7.9.2009 (Core)Docker version 23.0.6, buil…

VScode + opencv(cmake编译) + c++ + win配置教程

1、下载opencv 2、下载CMake 3、下载MinGW 放到一个文件夹中 并解压另外两个文件 4、cmake编译opencv 新建文件夹mingw-build 双击cmake-gui 程序会开始自动生成Makefiles等文件配置&#xff0c;需要耐心等待一段时间。 简单总结下&#xff1a;finish->configuring …

Linux的make和Makefile

目录 一、 介绍二、快速使用三、依赖关系和依赖方法四、语法 一、 介绍 1、makefile带来的好处就是——“自动化编译”&#xff0c;一旦写好&#xff0c;只需要一个make命令&#xff0c;整个工程完全自动编译&#xff0c;极大的提高了软件开发的效率。 2、make是一个命令工具&…

设计模式——适配器模式(Adapter Pattern)+ Spring相关源码

文章目录 一、适配器模式定义二、例子2.1 菜鸟教程例子2.1.1 定义两个不兼容的播放接口MediaPlayer 、AdvancedMediaPlayer2.1.2 定义AdvancedMediaPlayer两个实现类VlcPlayer 、Mp4Player2.1.3 定义适配器MediaAdapter2.1.4 定义AudioPlayer 并使用MediaAdapter2.1.5 使用 2.2…

Python异常处理:三种不同方法的探索与最佳实践

Python异常处理&#xff1a;三种不同方法的探索与最佳实践 前言 本文旨在探讨Python中三种不同的异常处理方法。通过深入理解各种异常处理策略&#xff0c;我们可以更好地应对不同的编程场景&#xff0c;选择最适合自己需求的方法。 异常处理在编程中扮演着至关重要的角色。合…

Win10共享打印机,别人连接不上出现无法连接到打印机错误码0x0000011b

环境&#xff1a; Win10 专业版 惠普L1119 问题描述&#xff1a; Win10共享打印机&#xff0c;别人连接不上出现无法连接到打印机错误码0x0000011b 解决方案&#xff1a; 1.打开我这台电脑的注册表找到 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Print在右侧…

JVM之类加载器

文章目录 版权声明类加载器类加载器的分类启动类加载器拓展类加载器&应用程序类加载器 双亲委派机制解决三个问题 打破双亲委派机制自定义类加载器案例演示线程上下文类加载器案例梳理OSGi模块化 版权声明 本博客的内容基于我个人学习黑马程序员课程的学习笔记整理而成。我…

http客户端简单demo

socket.h头文件 #pragma once #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <string> using std::string;namespace MySocket …

KVM虚拟机迁移原理与实践

虚拟机迁移 迁移(migration)包括系统整体的迁移和某个工作负载的迁移&#xff0c;系统整体迁移是将系统上的所有软件&#xff0c;包括操作系统&#xff0c;完全复制到另一台物理硬件机器上&#xff0c;而工作负载迁移仅仅迁移特定的工作负载。 虚拟化技术的出现&#xff0c;丰…

android源码添加adb host支持

本文开始参考在 android 上使用 adb client-CSDN博客&#xff0c;在shell中已经可以使用。但当我想在app中用 String command "/data/local/tmp/adb -s 307ef90dc8128844 shell ls";StringBuilder output new StringBuilder();try {Process process Runtime.getR…

Linux学习第二枪(yum,vim,g++/gcc,makefile的使用)

前言&#xff1a;在我的上一篇Linux博客我已经讲了基础指令和权限&#xff0c;现在我们来学习如何在Linux上运行和执行代码 目录 一&#xff0c;yum 二&#xff0c;vim 1&#xff09;命令行模式 2&#xff09;插入模式 3&#xff09;底行模式 三&#xff0c;gcc/g 四&a…

MATLAB的编程与应用,匿名函数、嵌套函数、蒙特卡洛法的掌握与使用

目录 1.匿名函数 1.1.匿名函数的定义与分类 1.2.匿名函数在积分和优化中应用 2.嵌套函数 2.1.嵌套函数的定义与分类 2.2.嵌套函数彼此调用关系 2.3.嵌套函数在积分和微分中应用 3.微分和积分 4.蒙特卡洛法 4.1.圆周率的模拟 4.2.计算N重积分&#xff08;均匀分布&am…

PHP·解决http_build_query模拟浏览器请求多选参数加下标索引的BUG| 无法模拟浏览器多选参数问题

$form_params array(id > 1,ids > [1,2,3], ); $form_params http_build_query($form_params); $form_params preg_replace(/%5B[0-9]%5D/simU, %5B%5D, $form_params);参考 http_build_query — 生成 URL-encode 之后的请求字符串

计算机提示找不到xinput1_3.dll怎么办?6个xinput1_3.dll丢失完美解决方案分享

xinput1_3.dll是Windows操作系统中的一个重要动态链接库文件&#xff0c;它负责处理游戏控制器和其他输入设备的相关功能。当计算机出现xinput1_3.dll缺失的问题时&#xff0c;可能会导致无法正常使用游戏控制器或其他输入设备。下面是针对这个问题的6个解决方法&#xff1a; 方…

基于html+jquery开发的科学计算器(课程作业)

基于html和jquery开发的科学计算器&#xff0c;该科学计算器可进行乘方、开方、指数、对数、三角函数、统计等方面的运算&#xff0c;又称函数计算器。 科学型带有所有普通的函数&#xff0c;所有的函数都分布在键盘上以致于你可以不用通过菜单列表来使用它们。 科学计算器支持…

微服务-我对Spring Clound的理解

官网&#xff1a;https://spring.io/projects/spring-cloud 官方说法&#xff1a;Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具&#xff08;例如配置管理、服务发现、熔断器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话…