Rust闭包的虫洞穿梭
1. 闭包是什么
闭包(Closure)的概念由来已久。无论哪种语言,闭包的概念都被以下几个特征共同约束:
- 匿名函数(非独有,函数指针也可以);
- 可以调用闭包,并显式传递参数(非独有,函数指针也可以);
- 以变量形式存在,可以传来传去(非独有,函数指针也可以);
- 可以在闭包内直接捕获并使用定义所处作用域的值(独有);
神奇的是最后一点,理解起来也比较别扭的,习惯就好了。
为了说明上述特征,可以看一个Rust例子。
fn display<T>(age: u32, print_info: T)
where T: Fn(u32)
{
print_info(age);
}
fn main() {
let name = String::from("Ethan");
let print_info_closure = |age|{
println!("name is {}", name);
println!("age is {}", age);
};
let age = 18;
display(age, print_info_closure);
}
运行代码:
name is Ethan age is 18
首先,闭包作为匿名函数存在了print_info_closure
栈变量中,然后传递给了函数display
作为参数,在display
内部调用了闭包,并传递了参数age
。最后神奇的事情出现了:在函数display
中调用的闭包居然打印出了函数main
作用域中的变量name
。
wormhole
闭包的精髓,就在于它同时涉及两个作用域,就仿佛打开了一个"虫洞",让不同作用域的变量穿梭其中。
let x_closure = ||{};
单独一行代码,就藏着这个奥妙:
- 赋值
=
的左侧,是存储闭包的变量,它处在一个作用域中,也就是我们说的闭包定义处的环境上下文; - 赋值
=
的右侧,那对花括号{}
里,也是一个作用域,它在闭包被调用处动态产生;
无论左侧右侧,都定义了闭包的属性,天然的联通了两个作用域。
对于闭包,Rust如此,其他语言也大抵如此。不过,Rust不是还有所有权、生命周期这一档子事儿么,所以还可以深入分析下。
2. Rust闭包捕获上下文的方式
如本篇题目,Rust闭包如何捕获上下文?
换个问法,main
作用域中的变量name
是以何种方式进入闭包的作用域的(第1节例子)?转移or借用?
It Depends,视情况而定。
Rust在std中定义了3种trait:
- FnOnce:闭包内对外部变量存在转移操作,导致外部变量不可用(所以只能call一次);
- FnMut:闭包内对外部变量直接使用,并进行修改;
- Fn:闭包内对外部变量直接使用,不进行修改;
后者能办到的,前者一定能办到。反之则不然。所以,编译器对闭包签名进行推理时:
- 实现FnMut的,同时也实现了FnOnce;
- 实现Fn的,同时也实现了FnMut和FnOnce。
第1节的例子,将display
的泛型参数从Fn改成FnMut,也可以无警告通过。
fn display<T>(age: u32, mut print_info: T)
where T: FnMut(u32)
{
print_info(age);
}
对环境变量进行捕获的闭包,需要额外的空间支持才能将环境变量进行存储。
3. 作为参数的闭包签名
上面代码display
函数定义,要接受一个闭包作为参数,揭示了如何显式的描述闭包的签名:在泛型参数上添加trait约束,比如T: FnMut(u32)
,其中(u32)
显式的表示了输入参数的类型。尽管是泛型参数约束,但是函数签名(除了没有函数名)描述还是非常精确的。
顺便说一句,Rust的泛型真的是干了不少事情,除了泛型该干的,还能添加trait约束,还能描述生命周期。
描述签名是一回事,但是谁来定义闭包的签名呢?闭包定义处,我们没有看到任何的类型约束,直接就可以调用。
答案是:闭包的签名,编译器全部一手包办了,它会将首次调用闭包传入参数和返回值的类型,绑定到闭包的签名。这就意味着,一旦闭包被调用过一次后,再次调用闭包时传入的参数类型,就必须是和第一次相同。
传入参数和返回值类型绑定好了,但你心中难免还会有一丝忧愁:描述生命周期的泛型参数肿么办?
Rust编译器也搞得定。
fn main(){
let lifttime_closure = |a, b|{
println!("{}", a);
println!("{}", b);
b
};
let a = String::from("abc");
let c;
{
let b = String::from("xyz");
c = lifttime_closure(&a, &b);
}
println!("{}", c);
}
以上代码无法通过编译,成功检测出了悬垂引用:
error[E0597]:
b
does not live long enough
显然,对于闭包,编译期可以对引用的生命周期进行检查,以保证引用始终有效。
这个例子,与其解释闭包与函数的区别,不如解释匿名函数与具名函数的区别:
- 具名函数是签名在先的,对于编译器来说,调用方和函数内部实现,只要分别遵守签名的约定即可。
- 匿名函数的签名则是被推理出来的,编译器要看全看透调用方的实际输入,以及函数内部的实际返回,检查自然也就顺带做掉了。
4. 函数返回闭包
第1节的例子,我们将一个闭包作为函数参数传入,那么根据闭包的特性,它应该能够作为函数的返回值。答案是肯定的。
基于前面介绍的Fn trait,我们定义一个返回闭包的函数,代码如下:
fn closure_return() -> Fn() -> (){
||{}
}
可是,编译失败了:
error[E0746]: return type cannot have an unboxed trait object doesn't have a size known at compile-time
失败信息显示,编译器无法确定函数返回值的大小。一个闭包有多大呢?并不重要。
开门见山,通用的解决方法是:为了能够返回闭包,可以使用一次装箱,从而将栈内存变量装箱存入堆内存,这样无论闭包有多大,函数返回值都是一个确定大小的指针。下面的代码里,使用Box::new
即可完成装箱。
fn closure_inside() -> Box<dyn FnMut() -> ()>
{
let mut age = 1;
let mut name = String::from("Ethan");
let age_closure = move || {
name.push_str(" Yuan");
age += 1;
println!("name is {}", name);
println!("age is {}", age);
};
Box::new(age_closure)
}
fn main(){
let mut age_closure = closure_inside();
age_closure();
age_closure();
}
运行结果如下:
name is Ethan Yuan age is 2 name is Ethan Yuan Yuan age is 3
上面的代码,除了让函数成功返回闭包之外,还有一个目的,我们想让闭包捕获函数内部环境中的值,但这次有些不同:
- 第1节代码示例,我们把外层的环境上下文,通过将闭包传入内层函数,这个不难理解,因为外层变量的生命周期更长,内层函数访问时,外层变量还活着;
- 而本节代码所做的,是通过闭包将内层函数的环境变量传出来给外层环境;
内层函数调用完成后就会销毁内层环境变量,那如何做到呢?幸好,Rust有所有权转移。只要能促成内层函数的环境变量向闭包进行所有权的转移,这个操作顺理成章。
正因为Rust具有所有权转移的概念,返回闭包(同时捕获环境变量)的机理,Rust的要比任何具有垃圾回收语言(JavaScript、Java、C#)的解释都更简单明了。后者总会给人一丝不安:内部函数调用都结束了,居然局部变量还活着。
代码中的所有权转移,这里使用了关键字move
,它可以在构建闭包时,强制将要捕获变量的所有权转移至闭包内部的特别存储区。需要注意的是,使用move
,并不影响闭包的trait
,本例中可以看到闭包是FnMut
,而不是FnOnce
。
- 文件上传漏洞的一些总结
- 任意文件下载引发的思考
- LSTM入门详解
- 如何将CDH集群JAVA升级至JDK8
- 如何将Kerberos环境下CDH集群JAVA升级至JDK8
- 干货|如何做准确率达98%的交通标志识别系统?
- 用57行代码搞定花8000万美元采购车牌识别项目
- Cloudera Manager Server服务在RedHat7状态显示异常分析
- 开源 | 基于Python的人脸识别:识别准确率高达99.38%!
- 转录组数据的基因表达变化情况探索
- 如何配置Kerberos服务的高可用
- 利用深度学习生成梵高风格画像
- 使用Python-Requests实现ODL对OVS的流表下发
- Keras入门必看教程
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 8个写JavaScript代码的小技巧
- .NET Core中间件与依赖注入的一些思考
- 如何审计MySQL 8.0中的分类数据查询?
- 聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
- Spring AOP,应该不会有比这更详细的介绍了!
- 我又发现 Spring Security 中一个小秘密!
- OpenCV的实用图像处理操作案例分享
- CentOS 7上搭建 Zabbix4.0,一次性成功,收藏了!
- 超全!我整理一波最常用的开源项目
- 【NLP】竞赛必备的NLP库
- Java NIO Selector 详解
- 【机器学习基础】一文搞懂机器学习里的L1与L2正则化
- 【深度学习】深入理解LSTM
- 短视频商城源码,两种方式实现点击出现弹窗显示
- 【50期】基础考察:ClassNotFoundException 和 NoClassDefFoundError 有什么区别