前言
闭包函数式编程范式的重要且优雅的实践,Rust作为一门现代化的编程语言,自然会吸收这种优秀设计,而在Rust中,由于可变性设计和借用检查机制,会使得Rust中闭包相对其他编程语言有更多理解难点。本文旨在用演绎的方式,辨析Rust中的闭包:Fn、FnOnce和FnMut
一、对闭包的基本认识
闭包的英文是closure,如果你从来没听过这个名词,可以简单理解,闭包就是一种特殊的函数,由于可以捕获环境变量,像一个包一样,环境变量捕获,所以称之闭包。以下是两段代码来解释什么是闭包
fn func_example(x:i32)->i32{x}
|x:i32|->i32{x};
闭包直观的特点:
- 不需要使用fn提示,正常函数需要fn提示,也没有函数签名,在C#或者Java中称为匿名函数
- 提示函数参数的部分再使用"()".,而是使用“||” ,这个符号,所以我个人也叫它直接函数;另外,不需要函数签名,直接定义函数体,也很符合这个称呼。
那么,所谓的捕获环境变量是怎么一回事?让我们看代码
let num=1;
let closure=|addend:i32|->i32 {num+addend};
let result= closure(1);
证明一下,普通函数的函数体不能直接使用环境变量
- 闭包的所谓捕获就是
在闭包的花括号使用到了环境变量,普通函数无法捕获环境变量,只能通过通过正常传参。你可以把“()”或者"| |“想象成一张嘴,是参数的入口,而闭包不一样的地方是它的”{ }"部分是另一张嘴。 - 闭包可以赋值给变量
closure,其实定义一个普通函数也是可以这样赋值的 - 闭包赋值给变量
closure之后,变量closure就具备函数签名一样的特点,可以使用closure(1)这样的形式调用闭包。
二、闭包的作用
在对闭包建立了初步认识之后,我们会想,为什么要设计闭包这种特殊的函数?
fn cook(){
}
以上场景中,如果要做两道面点,那我得写两个cook函数,但是步骤一和步骤三其实是一样的,这样就存在代码冗余。有了闭包就可以这样做
fn cook<T>(func:F)
where F:Fn()
{
func();
}
cook(||{println!("{}","做饺子")});
cook(||{println!("{}","做包子")});
其实普通函数也实现了Fn这个Trait ,普通函数和闭包一样也可以作为函数的参数传入。下列代码可以证明。
fn make_dumpling(){println!("做饺子")}
fn make_baozi(){println!("做包子")}
fn cook<T>(func:F)
where F:Fn()
{
func();
}
cook(make_dumpling);
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()
{
func(count);
}
let dumpling_count=10;
let baozi_count=4;
cook(make_dumpling,dumpling_count);
cook(make_baozi,baozi_count);
反之,利用闭包捕获环境变量特性,就会变得极度优雅
fn cook<T>(func:F)
where F:Fn()
{
func();
}
cook(||{println!("做饺子{}个",12)});
cook(||{println!("做包子{}个",4)});
综上,我们闭包是一个没有函数签名并且可以直接在函数体使用环境参数的特殊函数。一般一个函数不会被复用的时候,我们可以用闭包的形式来代替。
注意:闭包妙用决不仅仅是这些,笔者后面会出相应的文章继续探讨。
三、闭包与环境变量可变性/所有权的有趣现象
闭包捕获环境,不会获取得起所有权,除非使用move关键字
let str=String::from("hello");
let len_closure=||{ str.len()};
let len= len_closure();
println!("str len:{}",str.len());
let str=String::from("hello");
let len_closure=move||{ str.len()};
let len= len_closure();
println!("str len:{}",str.len());
使用了move关键字的闭包,我称之为move闭包。move闭包 形式是move||{ 环境变量};在我的想象中,"{ }"就像一张深渊大口,把环境变量吞进去,所以move闭包之后对环境变量的消费,都是非法的,环境变量被吞没了,无法消费了。
Fn与FnOnce的区别
另一个有趣的现象是这样,如下图,在move闭包中捕获了环境,理论上闭包应该实现了FnOnce 这个Trait的,但是实际上还是一个Fn,如下图:
闭包捕获环境的可变性问题
在闭包中修改环境变量的行为中,我们会发现两个有趣的现象:此时的闭包类型为FnMut;由于环境变量num没有声明为 mut num,所以无法被更改
结尾
好的,本文到此结束,请各位读者,回答自己:
1.什么是Fn?
2.什么时候Fn会转变成FnOnce?
3. 什么时候Fn会转变成FnMut?