Rust中闭包:Fn、FnOnce和FnMut

感想来源:
《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_dumplingmake_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?

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值