Rust权威指南之函数式编程
admin
2024-03-23 11:33:38
0

一. 简述

Rust在设计中受到了函数式编程很大的影响。常见的函数式风格编程通常包括将函数当作参数、将函数作为其他函数的返回值或将函数赋给变量已备之后执行等。

二. 闭包

Rust中的闭包是一种可以存入变量或作为参数传递给其他函数的匿名函数。我们可以在一个地方创建闭包,然后在不同的上下文环境中调用该闭包完成运算。闭包可以从定义它的作用域中捕获值。

Rust中的闭包,也叫做lambda表达式或者lambda,是一类能够捕获周围作用域中变量的函数。

下面我们看一个闭包和普通函数的区别:

pub fn fun1(i: i32) -> i32 { i + 1 } // 定义加一的函数#[cfg(test)]
mod tests {use super::*;#[test]fn function_test() {let fun2 = |i: i32| -> i32 { i + 1 }; // 加一的闭包let fun3 = |i| { i + 1 }; // 这个rust可以自动类型推断assert_eq!(2, fun1(1));assert_eq!(2, fun2(1));assert_eq!(2, fun3(1));}
}

2.1. 捕获上下文

闭包本质上很灵活,闭包可以捕获上下文环境,即可移动,又可借用。闭包可以通过以下方式捕获变量:

  • 通过引用: &T

  • 通过可变引用: &mut T

  • 通过值: T

闭包优先通过引用来捕获变量,并且仅在需要时使用其他方法。

2.1.1. 通过引用

下面我们可以先看一个捕获上下文:引用

#[test]
fn function_test() {let color = String::from("green");let print = || println!("color: {}", color); // 这个闭包打印 `color`。它立即借用(通过引用,`&`)`color`并将该借用和闭包本身存储到print变量中。`color`会一直保持被借用状态知道`print`离开作用域print();
}

2.1.2. 通过可变引用

接着我们看一下通过可变引用捕获变量:

#[test]
fn function_test() {let mut count = 0;let mut inc = || {count += 1;println!("`count`: {}", count);};inc();
}

上面的闭包的例子使count的值增加,当前闭包需要拿到&mut count,在闭包inc的前面添加mut,这是因为闭包利民啊存储着一个&mut变量。调用闭包时,该变量的变化就意味着闭包内部发生了变化。因此闭包需要是可变的。下面我们看一个错误的情况,这样也可以证明inc闭包使用的可变引用!

#[test]
fn function_test() {let mut count = 0;let mut inc = || {count += 1;println!("`count`: {}", count);};inc();let count_2 = &count; // @1 error cannot borrow `count` as immutable because it is also borrowed as mutableinc(); 
}

此时只要在@1之后不再使用inc闭包,我们就可以可变引用或者不可变引用了,如下:

#[test]
fn function_test() {let mut count = 0;let mut inc = || {count += 1;println!("`count`: {}", count);};inc();let count_2 = &count;assert_eq!(&1, count_2); // OK// let count_3 = &mut count;// *count_3 = 10;// assert_eq!(&10, count_3); // OK
}

2.1.3. 通过值

#[test]
fn fun_test() {let x = Box::new(3);let consume = || {println!("movable: {:?}", x);mem::drop(x);};consume();
}

2.1.4. move

在竖线之前使用move会强制闭包取得被捕获变量的所有权:

#[test]
fn move_test() {let haystack = vec![1, 2, 3];let contains = move | needle | haystack.contains(needle);assert_eq!(true, contains(&1));assert_eq!(false, contains(&4));assert_eq!(3, haystack.len()); // 此时测试失败,是因为借用检查不允许在变量被移动走之后再继续使用它
}

2.2. 作为输入参数

虽然Rust无需类型说明就能在大多数完成变量捕获,但是编写函数时,这种模糊写法是不允许的。当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下trait中的一种来指定的。其受限制程度按以下顺序递减,闭包大体分为三种类型:

  • Fn:表示不会方式为通过引用(&T:只是获取了不可变的引用变量)的闭包;

  • FnMut:表示捕获方法为通过可变引用(&mut T:只获取可变引用的变量)的闭包;

  • FnOnce:表示捕获方法为通过值(T:拿到变量的所有权而非借用)的闭包;

下面我们通过例子看一下

2.2.1. FnOnce

这种类型的闭包会获取变量的所有权,生命周期只能在当前作用域,之后就会释放了。

#[derive(Debug)]
struct Example<'a> {data: &'a str
}impl<'a> Drop for Example<'a> {fn drop(&mut self) {println!("释放Example");}
}fn fn_once(func: F) where F: FnOnce() {println!("fn_once 开始执行之前");func();// func(); @1println!("fn_once 开始执行之后");
}#[cfg(test)]
mod tests {use super::*;#[test]fn fn_once_test() {let example = Example { data: "hello" };let f = || println!("fn once 调用: {:?}", example);fn_once(f);println!("执行完毕");}
}

此时执行测试:

fn_once 开始执行之前
fn once 调用: Example { data: "hello" }
fn_once 开始执行之后
执行完毕
释放Example

如果把@1的代码放开编译器就会报错,原因我们可以从FnOnce的定义中找到,参数类型是self,在闭包执行第一次之后之前捕获的变量就释放了,无法执行第二次了。

pub trait FnOnce {type Output;extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}

2.2.2. FnMut

我们将上面的例子增加一个fn_mut方法和单元测试

fn fn_mut(num: u32, mut func: F) where F: FnMut(u32) {println!("fn_once 开始执行之前");func(num);func(num + 1);println!("fn_once 开始执行之后");
}#[test]
fn fn_mut_test() {let mut example = Example { data: "hello" };let f = |num: u32| {example.data = if num > 1 { "hello world" } else { "world" };println!("fn_mut 调用: {:?}", example);};fn_mut(1, f);println!("执行完毕");
}

执行单元测试如下:

fn_mut 开始执行之前
fn_mut 调用: Example { data: "world" }
fn_mut 调用: Example { data: "hello world" }
fn_mut 开始执行之后
执行完毕
释放Example

接着我们在看一下FnMut的定义,很明显我们看到参数类型是:&mut self,这种类型的闭包是可变借用,会改变变量,但不会释放该变量。

pub trait FnMut: FnOnce {extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}

2.2.3. Fn

最后看一下Fn的源码,参数类型是&self,此类型的闭包是不变借用,不会改变变量,也不会释放该变量。

pub trait Fn: FnMut {extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}

接着我们在上面的例子加上一个方法和单元测试:

fn fn_fn(func: F) where F: Fn() {println!("fn_once 开始执行之前");func();func();println!("fn_once 开始执行之后");
}#[test]
fn fn_test() {let example = Example { data: "hello" };let f = || {println!("fn once 调用: {:?}", example);};fn_fn(f);println!("执行完毕");
}

三. 迭代器

在设计模式的迭代器模式允许你依次为序列中每一个元素执行某些任务。迭代器会着这个过程中负责便利每一个元素并决定序列合适结束。只要使用了迭代器,我们就可以避免手动去实现这些逻辑。

在Rust中,迭代器时惰性的。当我们创建迭代器后,除非你主动调用方法来消耗并使用迭代器,否则它们不会产生任何的实际效果。Rust中的集合提供了三个公共方法创建迭代器:iter()iter_mut()into_iter(),分别用于迭代&T(引用)、&mut T(可变引用)和T(值)。

Rust的for循环器时时迭代器语法糖。

3.1. 迭代器的使用

这里我们先使用vec!创建一个动态数组,使用迭代器遍历:

#[test]
fn iter_test() {let v1 = vec![1, 2, 3, 4, 5];let v1_iter = v1.iter();for item in v1_iter {println!("{}", item);}
}

上面的代码时迭代器最常用的使用方式。Rust中所有的迭代器都实现了定义于标准库中Iterator trarit

pub trait Iterator {type Item;fn next(&mut self) -> Option;// 省略其他默认实现
}

这里的type ItemSelf::Item,它们定义了trait的关联类型,后续章节会填坑。

Iterator需要我们实现next方法,它会在每次被调用时返回一个包裹在Some中的迭代器元素,并在迭代结束的时候返回None

#[test]
fn iter_test() {let v1 = vec![1, 2, 3];let mut v1_iter = 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));assert_eq!(v1_iter.next(), None);
}

这里每次调用next方法改变了迭代器内部记录的序列位置的状态。在for循环中没有要求v1_iter可变,是因为循环取得了v1_iter的所有权并在内部使得它可变了。

这里iter()拿到的是一个不可变引用的迭代器,我们通过next取得的值实际上是指向动态数组中各个元素的不可变引用,如果我们需要一个取得v1所有权并返回元素本省的迭代器,此时我们就可以使用into_iter();如果我们需要可变引用的迭代器,那么我们可以使用iter_mut()方法。

3.2. 消费和适配

标准库中提供了很多默认实现的方法,其中那些调用next的方法也被称为消耗适配器,因为它们同样消耗了迭代器本身;还定义了了另外的一些被称为迭代器适配器,这些可以使将已有的迭代器转换称为其他不同类型的迭代器,我们可以链式调用多个迭代器适配器完成了一些复杂的操作。但是需要注意所有迭代器都是惰性的,所有你必须调用一个消耗适配器的方法才能从迭代器适配器中获取结果。

3.2.1. 消耗适配器

消耗迭代器之后无法继续被随后的代码使用。例子:

#[test]
fn iter_consume_test() {let v1 = vec![1, 2, 3, 4, 5];let total: i32 = v1.iter().sum();assert_eq!(15, total);
}

3.2.2. 迭代适配器

我们通过map方法生成的新的迭代器并返回的结果收集到一个动态数组中。

#[test]
fn iter_consume_test() {let v1 = vec![1, 2, 3, 4, 5];let x1: Vec = v1.iter().map(|x| x + 1).collect();println!("{:?}", v1); // [1, 2, 3, 4, 5]println!("{:?}", x1); // [2, 3, 4, 5, 6]
}

下面我们在看一个复杂的例子:

#[derive(PartialEq, Debug)]
struct Shoe<'a> {size: u32,style: &'a str,
}fn shoes_in_my_size(shoes: Vec, shoe_size: u32) -> Vec {shoes.into_iter() // 创建一个可以获取数组所有权的迭代器.filter(|s| s.size == shoe_size) // 过滤值适配成一个新的迭代器.collect() // 
}#[test]
fn filters_by_size() {let shoes = vec![Shoe { size: 10, style: "sneaker" },Shoe { size: 13, style: "sandal" },Shoe { size: 10, style: "boot" },];let in_my_size = shoes_in_my_size(shoes, 10);assert_eq!(in_my_size,vec![Shoe { size: 10, style: "sneaker" },Shoe { size: 10, style: "boot" },])
}

3.3. 自定义迭代器

下面我们看看如果自定义实现我们的迭代器,其实很简单我们只需要提供一个next方法的定义即可实现Iterator。一旦完成该方法,它就可以使用一起拥有默认实现的Iterator提供的方法。我们先定义一个结构体:

#[derive(Debug)]
struct Counter {count: u32,max: u32,
}impl Counter {fn new(max: u32) -> Counter {Counter { count: 0, max }}
}

接着我们实现Iteratornext方法:

impl Iterator for Counter {type Item = u32;fn next(&mut self) -> Option {self.count += 1;if self.count <= self.max {Some(self.count)} else {None}}
}

此时我们就可以拥有了一个迭代器,下面单元测试:

#[test]
fn calling_next_directly() {let mut counter = Counter::new(3);assert_eq!(counter.next(), Some(1));assert_eq!(counter.next(), Some(2));assert_eq!(counter.next(), Some(3));assert_eq!(counter.next(), None);
}

下面我们进行一些复杂的操作:将一个Counter实例产生的值与另一个Counter跳过首元素的值一一配对,接着将配对的两个值相乘,最后再对乘积能被3整除的那些数字求和。

#[test]
fn using_other_iterator_trait_methods() {let sum: u32 = Counter::new(3).zip(Counter::new(5).skip(1)).map(|(a, b)| a * b).filter(|x| x % 3 == 0).sum();assert_eq!(18, sum);
}

这里zip方法只会产生三对值,它在两个迭代器中任意一个返回None时结束迭代,所有不会出现(None, 4)

更多的关于自定义迭代器的创建可以参看:手把手带你实现链表。

3.4. 迭代器性能

Rust在迭代器和闭包中做很多优化,最终产出的代码极为高效。所有我们完全无所畏惧的使用迭代器和闭包!它们既能够让代码在观感上保持高层次的抽象,又不会因此带来任何运行时性能损耗。

3.5. 常用方法汇总

下面我们例子一些在迭代器中常用的方法:

方法功能
all/anyall测试迭代器的每个元素是否与谓词匹配;any相反
collect将迭代器转换为集合
copied创建一个迭代器,该迭代器将复制其所有元素
count消耗迭代器,计算迭代次数并返回它
enumerate创建一个迭代器,该迭代器给出当前迭代次数以及下一个值(i, val);其中 i 是当前迭代索引,val 是迭代器返回的值
eq/ne/lt/gt/le/ge两个迭代器元素的比较
filter创建一个迭代器,该迭代器使用闭包确定是否应产生元素
find搜索满足谓词的迭代器的元素
for_each在迭代器的每个元素上调用一个闭包;等效于for循环
inspect调试代码的时候使用
last消耗迭代器,返回最后一个元素
map通过其参数将一个迭代器转换为另一个迭代器: 实现 FnMut
max/``min返回迭代器最大/小元素
nth返回迭代器第几个元素
position/rposition在迭代器中搜索元素,并返回其索引,rposition是从右侧开始搜索
product遍历整个迭代器,将所有元素相乘
rev反转
fold累加
skip创建一个跳过前 n 个元素的迭代器
sum对迭代器元素求和
take创建一个迭代器,它产生第一个 n 元素,如果底层迭代器提前结束,则产生更少的元素
zip/unzip将两个迭代器压缩为成对的单个迭代器,unzip相反

更多的使用方法可以去看中文的标准文档:https://rustwiki.org/zh-CN/std/iter/trait.Iterator.html#

相关内容

热门资讯

求一首非常另类的中文歌曲,在K... 求一首非常另类的中文歌曲,在KTV一唱就能带动全场气氛的,最好是搞笑的!来来~ 小猪我向阁下推荐几首...
家风是什么班会 家风是什么班会传统习俗 家规 家训 礼仪 等等 具有很强的约束力
家乡的变化手抄报。 家乡的变化手抄报。 资料:在一个美丽的星期六,我会到我非常想念的老家。我的老家是一个,春天阳光明...
好看穿越电视剧 好看穿越电视剧除了神话 寻秦记 穿越时空的爱恋 最好是现穿古你可以期待一下有部新剧《宫...
求网游小说推荐。谢谢! 求网游小说推荐。谢谢!失落叶――――《 网游之纵横天下》 游戏生涯 作者将诸多元素完美的融合到一部大...
百变机兽之洛洛历险记 百变机兽之洛洛历险记洛洛的死敌,也就是猛兽族的机战王,他(她)叫什么?最早出现在第几集?晶晶,40集...
“势不可挡”是什么意思? “势不可挡”是什么意思?一个人是谁吗……不够的问题……不可抵挡的意思势不可挡的意思是来势迅猛,不可抵...
一个女人发表说说写的我要正正经... 一个女人发表说说写的我要正正经经追一个女生是啥意思一种游戏,赞或者评论了那条说说的人就要发一条一样的...
有几次睡得迷迷糊糊听到妈妈叫我... 有几次睡得迷迷糊糊听到妈妈叫我这是什么意思想家了/常回家看看啊呵呵 梦境是你的潜意识的体现,虽然在真...
你是怎么理解“相濡以沫,不如相... 你是怎么理解“相濡以沫,不如相忘于江湖”这句话的?意思就是说两个人在一起有时候倒不如分开的好,差不多...
后妈有好的吗 后妈有好的吗有人说后妈都很坏,不是自己的孩子,对他不好也是理所当然。当然不能以偏概全,有的后妈还是很...
不知道为什么心理很抵触跟许多人... 不知道为什么心理很抵触跟许多人接触,觉得很多人都恶意满满,不想跟他们有多少交流?其实你是有点恐交症,...
米其林大厨偷偷学的街头摊技巧,... 在人们的印象中,米其林餐厅代表着精致、高雅与顶级的烹饪技艺,其菜品往往经过精心雕琢,摆盘精美得如同艺...
原创 低... 作者︱懂酒哥 当下,白酒行业的深度调整依然在持续,“低度潮饮”却逆势崛起,成为酒业竞争的新高地。 近...
原创 猪... 猪肝,作为营养丰富的食材,富含铁、维生素A等众多对人体有益的成分,是补血明目的佳品。无论是爆炒猪肝的...
原创 香... 宝妈们,平时给孩子弄吃的有没有愁过?特别是那些又要好吃、又得有营养的小零食,总觉得翻来覆去就那么几样...
原创 茅... 在白酒的璀璨星空中,茅台无疑是最为耀眼的存在。它承载着数百年的酿酒智慧,以醇厚的口感、独特的风味,成...
一年内多次发布闭馆通知,青岛大... 齐鲁晚报·齐鲁壹点记者 周小涵近日,有网友反映,青岛胶州市大沽河博物馆长期闭馆未开放,引发关注。 网...
湖北恩施:山水秘境,避暑天堂 ... 7月7日,在恩施州巴东县三里城哨棚顶景区,当地的土家族村民为游客表演“撒叶儿嗬”舞蹈。湖北省恩施土家...
手机坏了,去哪儿维修? 手机坏了,去哪儿维修?无论你是在网上买的,还是在实体店买的,都是售后的,可以去找售后啊