- 闭包
闭包
- 闭包是把代码和环境一起存储的数据结构
- 闭包会引用(捕获/Move)环境中的自由变量
- 成为闭包类型的一部分
- 闭包中有自由变量和局部变量两类
-
函数签名的解释
fn spwn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static,
F: FnOnce()->T
F是一个闭包类型Send
修饰的类型的所有权可以在线程间安全的发送'static
修饰的类型- 拥有静态生态周期
- 或拥有所有权的类型
F: Send + 'static
- 这个闭包有静态生命周期或者有所有权
- 代码是静态生命周期
- 被捕获的变量也必须是静态生命周期或有所有权
- 所以
thread::spawn
才要move
关键字修饰
闭包是什么
- 闭包是一个匿名类型
- 声明后就会生成一个新的类型(与代码关联)
- 且会包含捕获(Ref/Move)的变量
- 闭包捕获的变量
- 先是引用
- 有move则移动所有权
- 而且闭包的大小,只与捕获的变量有关
- 与参数和局部变量无关
- 因为它们是调用时在栈上分配的
- 不会放到闭包结构体中
-
捕获变量的位置
let c = move || println!("name: {:?}, map: {:?}", name, hashmap);
这个闭包的自由变量,相当于
struct Closure { name: String, hashmap: HashMap<&str, &str>, }
结构体中的顺序和捕获顺序一样,但编译器会自动优化变量的顺序来减少空间
- 闭包只包含自由变量,却没有包含指向代码的函数指针
不同语言的闭包设计
- 闭包捕获了环境中的变量,舍不得被捕获的变量的归属和生命周期变复杂
- 所以其他语言不能将它们放在栈上,跟随函数自动消除
- 必须要申请额外堆空间、动态分发(处理成函数指针)、额外的内存回收
- 所以多数语言的闭包开销很大
Rust的闭包
- 不用move时,只引用环境变量
- 只要编译通过,说明闭包引用时期不超过变量的生命周期,没有野指针问题
- 使用move时,变量所有权移动到闭包中
- 环境中再无此变量
- 由闭包负责释放此内存
- 不会有内存安全问题
- Rust为每个闭包生成一个新类型
- 让闭包与代码关联
- 不再需要函数指针
Rust闭包的类型
FnOnce
trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
call_once
把闭包内部的数据移动到了函数内部- 闭包内部的数据被转移,闭包不完整,无法再使用
-
闭包不转移内部数据,就不是FnOnce
let c = move |g: String| (g, name.clone());
这个闭包会clone内部的数据并返回,所以它不是FnOnce,可以执行多次
-
把它变成FnOnce,则无法多次执行
call_once("a".into(), c); fn call_once(name: String, c: impl FnOnce(String)->(String, String)) -> (String, String) { c(name) }
FnMut
trait FnMut<Args> : FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
- FnMut也有
call_once
方法 - 还有
call_mut
方法,传入&mut self
,可以多次调用
闭包使用的场景
- 作为函数参数
-
作为函数返回值
fn curry<T>(x: T) -> impl Fn(T) -> T where T: Mul<Output=T> + Copy { move |y| x * y }
-
为闭包实现trait
fn execute(cmd: &str, exec: impl Executor) { exec(cmd) } execute("cat /etc/passwd", |cmd: &str| { Ok(format!( "execute: env: {}, cmd: {}", env,cmd)); })