感想来源:
《Rust圣经》の闭包
文章目录
前言
闭包函数式编程范式的重要且优雅的实践,Rust作为一门现代化的编程语言,自然会吸收这种优秀设计,而在Rust中,由于可变性设计和借用检查机制,会使得Rust中闭包相对其他编程语言有更多理解难点。本文旨在用演绎的方式,辨析Rust中的闭包:Fn、FnOnce和FnMut
一、对闭包的基本认识
闭包的英文是closure,如果你从来没听过这个名词,可以简单理解,闭包就是一种特殊的函数
,由于可以捕获环境变量,像一个包一样,环境变量捕获,所以称之闭包。以下是两段代码来解释什么是闭包
//普通函数
fn func_example(x:i32)->i32{x}
//闭包
|x:i32|->i32{x};
闭包直观的特点:
- 不需要使用fn提示,正常函数需要fn提示,也没有函数签名,在C#或者Java中称为匿名函数
- 提示函数参数的部分再使用"()".,而是使用“||” ,这个符号,所以我个人也叫它直接函数;另外,不需要函数签名,直接定义函数体,也很符合这个称呼。
那么,所谓的捕获环境变量是怎么一回事?让我们看代码
//定义一个捕获环境变量「num」
let num=1;
//定义一个函数体中使用了环境变量num的闭包
let closure=|addend:i32|->i32 {num+addend};
//调用闭包
let result= closure(1);
证明一下,普通函数的函数体不能直接使用环境变量
以上例子我们可以看出:
- 闭包的所谓捕获就是
在闭包的花括号使用到了环境变量
,普通函数无法捕获环境变量,只能通过通过正常传参。你可以把“()”或者"| |“想象成一张嘴,是参数的入口,而闭包不一样的地方是它的”{ }"部分是另一张嘴。 - 闭包可以赋值给变量
closure
,其实定义一个普通函数也是可以这样赋值的 - 闭包赋值给变量
closure
之后,变量closure
就具备函数签名一样的特点,可以使用closure(1)
这样的形式调用闭包。
二、闭包的作用
在对闭包建立了初步认识之后,我们会想,为什么要设计闭包这种特殊的函数?
//需求场景:写一个函数模拟做面点给家人吃的整个流程
fn cook(){
//步骤1:烧水
//步骤2: 蒸屉里放饺子
//步骤3: 端给家人吃
}
以上场景中,如果要做两道面点,那我得写两个cook函数
,但是步骤一和步骤三其实是一样的,这样就存在代码冗余。有了闭包就可以这样做
//需求场景:写一个函数模拟做面点给家人吃的整个流程
fn cook<T>(func:F)
where F:Fn()
//这里Fn()表示没有输入也没有输出的闭包
//看不懂的同学不用纠结,知道是闭包的标志就好后面会讲到。
{
//步骤1:烧水
//步骤2:
func(); //实际上要做什么菜怎么做用一个闭包变量替代
//步骤3: 端给家人吃
}
cook(||{println!("{}","做饺子")});
cook(||{println!("{}","做包子")});
其实普通函数也实现了Fn这个Trait ,普通函数和闭包一样也可以作为函数的参数传入。下列代码可以证明。
//需求场景:写一个函数模拟做面点给家人吃的整个流程
fn make_dumpling(){println!("做饺子")}
fn make_baozi(){println!("做包子")}
fn cook<T>(func:F)
where F:Fn()
//这里Fn()表示没有输入也没有输出的函数,一般是闭包
//看不懂的同学不用纠结,知道是闭包的标志就好后面会讲到。
{
//步骤1:烧水
//步骤2:
func(); //实际上要做什么菜怎么做用一个闭包变量替代
//步骤3: 端给家人吃
}
//cook(||{println!("做饺子")});
cook(make_dumpling);
//cook(||{println!("做包子")});
cook(make_baozi);
但是我们为什么不这么做呢,因为make_dumpling
和make_baozi
如果是一个次性函数,没有复用需求,但我们专门为他们定义一次函数的做法显得多余。
选择闭包很多时候也是因为其捕获变量的便利性,例如如果我们需要声明做包子和面点的数量 需要在make_dumpling
make_baozi
中加入参数, cook
函数新增参数
//需求场景:写一个函数模拟做面点给家人吃的整个流程
fn make_dumpling(count:i32){println!("做饺子{}个",count)}
fn make_baozi(count:i32){println!("做包子{}个",count)}
fn cook<T>(func:F,count:i32)
where F:Fn()
//这里Fn()表示没有输入也没有输出的函数,一般是闭包
//看不懂的同学不用纠结,知道是闭包的标志就好后面会讲到。
{
//步骤1:烧水
//步骤2:
func(count); //实际上要做什么菜怎么做用一个闭包变量替代
//步骤3: 端给家人吃
}
let dumpling_count=10;
let baozi_count=4;
//cook(||{println!("{}","做饺子")});
cook(make_dumpling,dumpling_count);
//cook(||{println!("{}","做包子")});
cook(make_baozi,baozi_count);
反之,利用闭包捕获环境变量特性,就会变得极度优雅
//需求场景:写一个函数模拟做面点给家人吃的整个流程
fn cook<T>(func:F)
where F:Fn()
//这里Fn()表示没有输入也没有输出的函数,一般是闭包
//看不懂的同学不用纠结,知道是闭包的标志就好后面会讲到。
{
//步骤1:烧水
//步骤2:
func(); //实际上要做什么菜怎么做用一个闭包变量替代
//步骤3: 端给家人吃
}
cook(||{println!("做饺子{}个",12)});
cook(||{println!("做包子{}个",4)});
综上,我们闭包是一个没有函数签名并且可以直接在函数体使用环境参数的特殊函数。一般一个函数不会被复用的时候,我们可以用闭包的形式来代替。
注意:闭包妙用决不仅仅是这些,笔者后面会出相应的文章继续探讨。
三、闭包与环境变量可变性/所有权的有趣现象
闭包捕获环境,不会获取得起所有权,除非使用move
关键字
//闭包不会获得环境变量所有权
let str=String::from("hello");
let len_closure=||{ str.len()};//捕获str
let len= len_closure();
println!("str len:{}",str.len());
//使用move关键字强制转移所有权
let str=String::from("hello");
let len_closure=move||{ str.len()};//捕获str
let len= len_closure();
println!("str len:{}",str.len());//此处开始报错
使用了move
关键字的闭包,我称之为move闭包
。move闭包
形式是move||{ 环境变量};在我的想象中,"{ }"就像一张深渊大口,把环境变量吞进去,所以move闭包
之后对环境变量的消费,都是非法的,环境变量被吞没了,无法消费了。
Fn与FnOnce的区别
另一个有趣的现象是这样,如下图,在move闭包
中捕获了环境,理论上闭包应该实现了FnOnce
这个Trait的,但是实际上还是一个Fn,如下图:
让我们小小改个东西,实验一下
上图中,我们在move闭包
中使用函数move_onership
消耗掉str之后,闭包才会真正变成FnOnce
。
那这个FnOnce 和Fn都是闭包,有什么区别呢?
FnOnce如其名,只能执行一次;而Fn可以像普通函数一样执行无限次。
为什么FnOnce只能执行一次? move闭包
捕获环境变量,相当于把食物含在口中,Fn闭包
每次执行,相当于在回味
这个环境变量,环境变量不会消耗;相反,FnOnce
调用的时候,相当于把环境变量咽进肚子
,一个环境变量不能被咽到肚子里第二次
。
闭包捕获环境的可变性问题
在闭包中修改环境变量的行为中,我们会发现两个有趣的现象:此时的闭包类型为FnMut
;由于环境变量num
没有声明为 mut num
,所以无法被更改
我们尝试把环境变量num
没有声明为 mut num
,并且尝试一下执行闭包。
然后我们会发现,此时闭包无法被执行。
我们稍微总结下,闭包定义时, 闭包想
修改环境变量,必须保证环境变量声明mut。想
修改环境变量的闭包为FnMut();
那FnMut闭包想要执行,还欠缺什么呢,让我们尝试在闭包变量声明mut
。
成功!!!
冷静!让我们总结一下,修改mut 环境变量
的行为使得闭包也成了mut闭包
,简称FnMut
,FnMut必须显示声明为mut 否则无法被调用。
其实这里我认为,这里代码检查不够严格,闭包和环境变量如果没有同时声明mut,就应该报错了,而不是等到闭包调用再来提示。
结尾
好的,本文到此结束,请各位读者,回答自己:
1.什么是Fn?
2.什么时候Fn会转变成FnOnce?
3. 什么时候Fn会转变成FnMut?