【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、闭包(Closures)
    • 1.1、闭包(Closures)
    • 1.2、Rust的闭包语法
      • 1.2.1、闭包语法形式
      • 1.2.1、闭包简化形式
    • 1.3、闭包的类型推导
    • 1.4、结构体中的闭包
    • 1.5、捕获环境中的值
      • 1.5.1、捕获环境的值
      • 1.5.2、闭包对内存的影响
      • 1.5.2、三种 Fn trait
        • 1.5.2.1、FnOnce
        • 1.5.2.2、FnMut
        • 1.5.2.3、Fn Trait
      • 1.5.3、move 和 Fn
      • 1.5.4、三种 Fn 的关系
    • 1.6、闭包作为函数返回值
  • 二、迭代器(Iterators)
    • 2.1、迭代器(Iterators)
    • 2.2、Iterator trait
      • 2.2.1、Iterator trait
      • 2.2.2、next 方法
    • 2.3、迭代方法
    • 2.4、消耗迭代器的方法
      • 2.4.1、sum 方法
      • 2.4.2、collect方法
    • 2.5、迭代器适配器
      • 2.5.1、map
      • 2.5.2、zip
      • 2.5.3、filter
      • 2.5.4、enumerate
    • 2.6、自定义迭代器
  • 四、性能对比:循环 VS 迭代器
  • 总结

前言

Rust的设计灵感来源于很多现存的语言和技术,其中一个显著的影响就是函数式编程(functional prigramming)。函数式编程风格通常包含将函数作为参数值或其他函数的返回值、将韩式赋值给变量以供之后执行等等。

主要教材参考 《The Rust Programming Language》


一、闭包(Closures)

1.1、闭包(Closures)

Rust的闭包(Closures)是可以保存进变量或作为参数传递给其他函数的匿名函数。可以在一个地方创建闭包,然后在不同的上下文执行闭包运算。不同函数,闭包允许捕获调用作用域中的值。
例如:

  • 使用函数作为参数进行传递
  • 使用函数作为函数返回值
  • 将函数赋值给变量

1.2、Rust的闭包语法

1.2.1、闭包语法形式

Rust闭包在形式上借鉴了 Smalltalk 和 Ruby 语言,与函数最大的不同就是它的参数是通过 |param| 的形式进行声明的。

示例:闭包语法形式

|param1,param2,....|{语句1;语句2...返回表达式
}

1.2.1、闭包简化形式

如果只有一个返回表达式可以简化成如下形式

|param|返回表达式

1.3、闭包的类型推导

Rust 是静态语言,因此所有的变量都具有类型,但是得益于编译器的强大类型推导能力,在很多时候我们并不需要显式声明类型,但是函数和必须为所有参数和返回值指定类型。

为了增加代码可读性,有时候我们会显式地给类型进行标注,出于同样的目的,也可以给闭包标注类型。

let sum = |x:i32, y:32| -> 32{x + y
}

类型推导虽然很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型。

1.4、结构体中的闭包

struct Cacher<T> where T: Fn(u32) -> u32 {query: T,value: Optional<u32>
}

此时 query 就是一个闭包,他的类型是 Fn(u32) -> u32 是一个特征,用来表示T 是一个闭包类型。

1.5、捕获环境中的值

1.5.1、捕获环境的值

闭包可以捕获环境中的值

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

如果你使用函数来实现,编译器会告诉我们无法捕获动态环境中的值。

1.5.2、闭包对内存的影响

当闭包从环境中捕获一个值时,会分配内存去存储这些值。对于有些场景来说,这种额外的内存分配会成为一种负担。与之相比,函数就不会去捕获这些环境值,因此定义和使用函数不会拥有这种内存负担。

1.5.2、三种 Fn trait

闭包捕获环境变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此Fn Trait 也有三种。

1.5.2.1、FnOnce

该类型的闭包会拿走被捕获变量的所有权。该闭包只能运行一次。

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()})
}

此时编译器就会报错,因为不能对已失去所有权的闭包变量进行二次调用。 错误提示告诉我们因为F没有实现Copy Trait ,所以会报错,那么我们添加约束,试试实现了Copy的闭包。

fn fn_once<F>(func: F)
whereF: FnOnce(usize) -> bool + Copy,// 改动在这里
{println!("{}", func(3));println!("{}", func(4));
}fn main() {let x = vec![1, 2, 3];fn_once(|z|{z == x.len()})
}

如果你想强制闭包取得捕获变量的所有权,那么可以在参数列表前面添加 move 关键字,这种用法通常用在闭包的生命周期大于捕获变量的生命周期时,例如将闭包返回或移入其他线程。

use std::thread;
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();

1.5.2.2、FnMut

它是可变借用的方式捕获了环境中的值,因此可以修改该值。

fn main() {let mut s = String::new();let mut update_string =  |str| s.push_str(str);update_string("hello");println!("{:?}",s);
}

复杂形式

fn main() {let mut s = String::new();let update_string =  |str| s.push_str(str);exec(update_string);println!("{:?}",s);
}fn exec<'a, F: FnMut(&'a str)>(mut f: F)  {f("hello")
}

1.5.2.3、Fn Trait

它是以不可变借用的方式捕获环境中的值,让我们把上面的代码中的 F 类型修改为Fn。

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")
}

从报错中很清晰的看出,我们的闭包实现的是 FnMut 特征,需要的是可变借用,但是在 exec 中却给它标注了 Fn 特征,因此产生了不匹配,再来看看正确的不可变借用方式:

fn main() {let s = "hello, ".to_string();let update_string =  |str| println!("{},{}",s,str);exec(update_string);println!("{:?}",s);
}fn exec<'a, F: Fn(String) -> ()>(f: F)  {f("world".to_string())
}

1.5.3、move 和 Fn

在上面我们讲解到 move 关键字对于 FnOnce的重要性,实际上使用了 move 的闭包依然可能实现了 Fn 和
Fn Mut 特征。

因为一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。move 本身强调的就是后者,闭包如何捕获变量:

1.5.4、三种 Fn 的关系

实际上,一个闭包不仅仅实现一种 Fn trait,规则如下

  • 所有的闭包都自动实现了 FnOnce 特征,因此任何一个闭包都至少可以被调用一次
  • 没有移出所有捕获变量的所有权的闭包自动实现了 FnMut Trait
  • 不需要对捕获变量进行改变的闭包自动失效了 Fn Trait
pub trait Fn<Args> : FnMut<Args> {extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}pub trait FnMut<Args> : FnOnce<Args> {extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}pub trait FnOnce<Args> {type Output;extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

1.6、闭包作为函数返回值

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)}
}

二、迭代器(Iterators)

2.1、迭代器(Iterators)

迭代器模式:对一系列项执行某些人物,迭代器负责遍历每一项,确定序列何时完成。Rust的迭代器是惰性的,除非调用消费迭代器的方法,否则迭代器本身没有任何效果。

迭代器允许我们迭代一个连续的集合,例如数组、向量、hashMap等。

let v1 = vec![1, 2, 3];let v1_iter = v1.iter();for val in v1_iter {println!("{}", val);
}

2.2、Iterator trait

2.2.1、Iterator trait

所有的迭代器都实现了 Iterator trait,该 Trait 定义于标准库。定义大致如下。

pub trait Iterator {type Item;fn next(&mut self) -> Option<Self::Item>;// 省略其余有默认实现的方法
}

2.2.2、next 方法

每次返回迭代器中的一项,返回结果包裹在Some 里面,迭代结束返回None。

fn main() {let arr = [1, 2, 3];let mut arr_iter = arr.into_iter();assert_eq!(arr_iter.next(), Some(1));assert_eq!(arr_iter.next(), Some(2));assert_eq!(arr_iter.next(), Some(3));assert_eq!(arr_iter.next(), None);
}

2.3、迭代方法

iter:在不可变引用上创建迭代器
into_iter: 创建的迭代器会获得所有权
iter_mut: 迭代可变的引用

2.4、消耗迭代器的方法

2.4.1、sum 方法

调用 next 方法的方法叫做 消耗型适配器。 例如 :Sum 方法:取得迭代器的所有权。

fn main() {let v1 = vec![1, 2, 3];let v1_iter = v1.iter();let total: i32 = v1_iter.sum();assert_eq!(total, 6);// v1_iter 是借用了 v1,因此 v1 可以照常使用println!("{:?}",v1);// 以下代码会报错,因为 `sum` 拿到了迭代器 `v1_iter` 的所有权// println!("{:?}",v1_iter);
}

2.4.2、collect方法

use std::collections::HashMap;
fn main() {let names = ["sunface", "sunfei"];let ages = [18, 18];let folks: HashMap<_, _> = names.into_iter().zip(ages.into_iter()).collect();println!("{:?}",folks);
}

zip 是一个迭代器适配器,它的作用就是将两个迭代器的内容压缩到一起,形成 Iterator<Item=(ValueFromA, ValueFromB)> 这样的新的迭代器,在此处就是形如 [(name1, age1), (name2, age2)] 的迭代器。

然后再通过 collect 将新迭代器中(K, V) 形式的值收集成 HashMap<K, V>,同样的,这里必须显式声明类型,然后 HashMap 内部的 KV 类型可以交给编译器去推导,最终编译器会推导出 HashMap<&str, i32>,完全正确!

2.5、迭代器适配器

2.5.1、map

既然消费者适配器是消费掉迭代器,然后返回一个值。那么迭代器适配器,顾名思义,会返回一个新的迭代器,这是实现链式方法调用的关键:v.iter().map().filter()…。

与消费者适配器不同,迭代器适配器是惰性的,意味着你需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:

let v1: Vec<i32> = vec![1, 2, 3];let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();assert_eq!(v2, vec![2, 3, 4]);

2.5.2、zip

2.5.3、filter

2.5.4、enumerate

使用 enumerate 方法可以获取迭代时的索引。
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i,v) in v.iter().enumerate() {println!("第{}个值是{}",i,v)
}

2.6、自定义迭代器

自定义迭代器很简单,我们只需要实现 Iterator 特征 以及next 方法即可。实际上  Iterator 之中还有其他方法,其他方法都有默认实现,无需手动去实现。
impl Iterator for Counter {type Item = u32;fn next(&mut self) -> Option<Self::Item> {if self.count < 5 {self.count += 1;Some(self.count)} else {None}}
}

四、性能对比:循环 VS 迭代器

迭代器是 Rust的零成本抽象(zero-cost abstractions) 之一,意味着抽象并不会引入运行时开销。

总结

以上就是今天要讲的内容

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

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

相关文章

Leetcode 191.位1的个数

编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中数字位数为 1 的个数&#xff08;也被称为汉明重量&#xff09;。 提示&#xff1a; 请注意&#xff0c;在某些语言&#xff08;如 Java&#xff09;中…

2023/08/27

一、图片引入 项目中往往不使用相对路径引入文件&#xff0c;一般都使用实现绝对路径引入文件。 方式一&#xff1a;【适用vue2&#xff0c;不适用vue3】 <img :src"require(/assets/images/home/bottom_can.png)" alt"">方式二&#xff1a;【适用…

mac m1 docker 安装kafka和zookeeper

获取本地ip地址 ifconfig en0 192.168.0.105. 下面的ip都会使用到 1、拉取镜像 docker pull wurstmeister/zookeeper docker pull wurstmeister/kafka 2、启动容器 启动 zookeeper docker run -d --name zookeeper -p 2181:2181 映射 3、 启动 kafka 注意&#xff…

计网-All

路由器的功能与路由表的查看_路由器路由表_傻傻小猪哈哈的博客-CSDN博客路由基础-直连路由、静态路由与动态路由的概念_MikeVane-bb的博客-CSDN博客路由器的功能与路由表的查看_路由器路由表_傻傻小猪哈哈的博客-CSDN博客 直连路由就是路由器直接连了一个网段&#xff0c;他就…

一个短视频去水印小程序,附源码

闲来无事&#xff0c;开发了一个短视频去水印小程序&#xff0c;目前支持抖音、快手&#xff0c;后续再加上别的平台。 因为平台原因&#xff0c;就不放二维码了&#xff0c;你可以直接微信搜索【万能老助手】这里贴一张效果图。 页面非常简单&#xff0c;这里就不过多介绍了&…

Git企业开发控制理论和实操-从入门到深入(五)|标签管理

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

正则表达式总结

作为软件工程师&#xff0c;工作中经常都需要使用正则表达式进行搜索&#xff0c;替换&#xff0c;验证数据&#xff08;手机号、邮箱、账号&#xff09;等。 但没有系统的学习总结过。现在就来学习总结一下。 认识元字符 元字符就是一些特殊符号&#xff0c;代表一些特殊意思…

初始Netty

文章目录 目录 文章目录 前言 一、netty 总结 前言 认识netty 一、netty Netty是一个基于Java的高性能网络应用框架&#xff0c;用于快速开发可扩展的网络服务器和客户端。它提供了易于使用的抽象API&#xff0c;使开发人员能够轻松地构建各种网络应用程序&#xff0c;包括…

JavaScript基础语法

一、JavaScript编写方式 位置一&#xff1a;HTML代码行内&#xff08;不推荐&#xff09; <!-- 方式一&#xff1a;行内编写 --> <a href"javascript:alert(hello world)">hello world</a> <!-- 方式二&#xff1a;行内编写&#xff0c;通过监…

学信息系统项目管理师第4版系列03_文件与标准

审核未通过&#xff0c;删除文件部分&#xff0c;仅保留标准化相关内容&#xff0c;重发 12. 标准化 12.1. 采用国际标准和国外先进标准的程度分为等同采用、修改采用和等效采用 3 种 12.1.1. 【高21上选20】 12.1.2. 采用指与国际标准在技术内容和文本结构上相同,或者与国…

期权是什么?期权的优缺点是什么?

期权是一种合约&#xff0c;有看涨期权和看跌期权两种类型&#xff0c;也就是做多和做空两个方向&#xff0c;走势标的物对应大盘指数&#xff0c;这也是期权与其他金融工具的主要区别之一&#xff0c;可以用于套利&#xff0c;对冲股票和激进下跌的风险&#xff0c;下文介绍期…

DML语句的用法(MySQL)

文章目录 前言一、DML介绍二、DML语句操作1、给指定字段添加数据2、给全部字段添加数据3、批量添加数据4、修改数据5、删除数据 总结 前言 本文主要介绍SQL语句中DML语句的用法。 在实验开始之前我们先创建一下所要使用表&#xff0c;如下图所示&#xff1a; 一、DML介绍 DM…

Modbus转Profinet网关连接三菱变频器博图快速配置

本案例将分享如何使用兴达易控的modbus转profinet网关&#xff08;XD-MDPN100&#xff09;来连接西门子1200系列plc&#xff0c;并实现三菱变频器的485通讯兼容转modbusTCP通信。通过在博图中进行配置&#xff0c;我们可以实现设备之间的连接和通信。 首先&#xff0c;我们需要…

Vue自定义指令- v-loading封装

Vue自定义指令- v-loading封装 文章目录 Vue自定义指令- v-loading封装01-自定义指令自定义指令的两种注册语法&#xff1a; 02自定义指令的值03-自定义指令- v-loading指令封装 01-自定义指令 什么是自定义指令&#xff1f; 自定义指令&#xff1a;自己定义的指令&#xff0c…

webassembly003 GGML Tensor Library part-1

GGML ggml的函数 可以看到官方示例项目仅依赖于#include "ggml/ggml.h"&#xff0c; #include "common.h"&#xff0c;可以阅读ggml.h获取ggml的使用帮助 函数解释注释ggml_tensor多维张量按行主顺序存储。ggml_tensor结构包含每个维度中元素数&#xf…

httpd协议与apache

1.http 相关概念 HTTP是处于应用层的协议&#xff0c;使用TCP传输层协议进行可靠的传送。因此&#xff0c;需要特别提醒的是&#xff0c;万维网是基于因特网的一种广泛因特网应用系统&#xff0c;且万维网采用的是HTTP&#xff08;80/TCP&#xff09;和 HTTPS&#xff08;443/…

OpenCV基础知识(8)— 图形检测

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。图形检测是计算机视觉的一项重要功能。通过图形检测可以分析图像中可能存在的形状&#xff0c;然后对这些形状进行描绘&#xff0c;例如搜索并绘制图像的边缘&#xff0c;定位图像的位置&#xff0c;判断图像中有没有直线、…

前端开发工具: VSCode

VSCode 安装使用教程&#xff08;图文版&#xff09; | arry老师的博客-艾编程 1. 下载 在官方网站&#xff1a;https://code.visualstudio.com/ 下载最新版本的 VSCode 即可 2. VSCode 常见插件安装 所有插件安装后,需要重启一下才生效 2.1 简体中文语言包 2.2 编辑器主…

webscoket在vue中的使用

项目场景&#xff1a; 提示&#xff1a;项目相关背景&#xff1a; 什么是webscoket&#xff1f;: WebSocket是一种计算机通信协议&#xff0c;通过单个TCP连接提供全双工通信信道。实现了web客户端和服务器之间的实时通信&#xff0c;与传统的HTTP连接相比&#xff0c;允许以…

Docker harbor私有仓库部署与管理

一、搭建本地私有仓库二、Harbor私有仓库部署与管理1、Harbor概述2、Harbor的特性3、Harbor的核心组件3.1 Proxy3.2 Registry3.3 Core services3.3.1 UI&#xff08;harbor-ui&#xff09;3.3.2 WebHook3.3.3 Token 服务 3.4 Database&#xff08;harbor-db&#xff09;3.5 Log…