正式开始
闭包的作用
- 作为参数传递给函数;
- 作为函数返回值;
- 实现某个 trait,使其能表现出其他行为
闭包的定义
闭包是 将函数或者说代码和其环境一起存储的一种数据结构
闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分
在 Rust 里,闭包可以用 |args| {code} 或者 move |args| {code} 来表述
以创建新线程的 thread::spawn为例
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
-
F: FnOnce() → T,表明 F 是一个接受 0 个参数、返回 T 的闭包
-
F: Send + 'static,说明闭包 F 这个数据结构,需要静态生命周期或者拥有所有权,并且它还能被发送给另一个线程
a. 使用了 move 且 move 到闭包内的数据结构满足 Send,因为此时,闭包的数据结构拥有所有数据的所有权,它的生命周期是 'static
-
T: Send + 'static,说明闭包 F 返回的数据结构 T,需要静态生命周期或者拥有所有权,并且它还能被发送给另一个线程
闭包本质上是什么?
- 闭包是一种匿名类型,一旦声明,就会产生一个新的类型
- 这个类型无法被其它地方使用
- 这个类型就像一个结构体,会包含所有捕获的变量
写代码探索
use std::{collections::HashMap, mem::size_of_val};
fn main() {
// c1 没有参数,也没捕获任何变量,从代码输出可以看到,c1 长度为 0;
let c1 = || println!("hello world!");
// c2 有一个 i32 作为参数,没有捕获任何变量,长度也为 0,可以看出参数跟闭包的大小无关
let c2 = |i: i32| println!("hello: {}", i);
let name = String::from("tyr");
let name1 = name.clone();
let mut table = HashMap::new();
table.insert("hello", "world");
// 如果捕获一个引用,长度为 8
// c3 捕获了一个对变量 name 的引用,这个引用是 &String,长度为 8。而 c3 的长度也是 8;
let c3 = || println!("hello: {}", name);
// 捕获移动的数据 name1(长度 24) + table(长度 48),closure 长度 72
// c4 捕获了变量 name1 和 table,由于用了 move,它们的所有权移动到了 c4 中。c4 长度是 72,恰好等于 String 的 24 字节,加上 HashMap 的 48 字节
let c4 = move || println!("hello: {}, {:?}", name1, table);
let name2 = name.clone();
// 和局部变量无关,捕获了一个 String name2,closure 长度 24
// c5 捕获了 name2,name2 的所有权移动到了 c5,虽然 c5 有局部变量,但它的大小和局部变量也无关,c5 的大小等于 String 的 24 字节
let c5 = move || {
let x = 1;
let name3 = String::from("lindsey");
println!("hello: {}, {:?}, {:?}", x, name2, name3);
};
println!(
"c1: {}, c2: {}, c3: {}, c4: {}, c5: {}, main: {}",
size_of_val(&c1),
size_of_val(&c2),
size_of_val(&c3),
size_of_val(&c4),
size_of_val(&c5),
size_of_val(&main),
)
}
-
不带 move 时,闭包捕获的是对应自由变量的引用;
-
带 move 时,对应自由变量的所有权会被移动到闭包结构中
-
闭包的大小跟参数、局部变量都无关,只跟捕获的变量有关
a. 参数和局部变量,因为它们是在调用的时刻才在栈上产生的内存分配,说到底和闭包类型本身是无关的
结合gdb查看
- c3 的确是一个引用,把它指向的内存地址的 24 个字节打出来,是 (ptr | cap | len) 的标准结构。如果打印 ptr 对应的堆内存的 3 个字节,是 ‘t’ ‘y’ ‘r’
- c4 捕获的 name 和 table,内存结构和下面的结构体一模一样
struct Closure4 {
name: String, // (ptr|cap|len)=24字节
table: HashMap<&str, &str> // (RandomState(16)|mask|ctrl|left|len)=48字节
}
闭包捕获变量的顺序,和其内存结构的顺序是一致的(在逻辑上是一致的)。但由于Rust编译器会重排内存,所以有些情况下,内存中数据的顺序可能和struct不一致
- 调整闭包里使用 name1 和 table 的顺序
let c4 = move || println!("hello: {:?}, {}", table, name1); - 其数据的位置是相反的,类似于
struct Closure4 {
table: HashMap<&str, &str> // (RandomState(16)|mask|ctrl|left|len)=48字节
name: String, // (ptr|cap|len)=24字节
}
- 从gdb查看结果
小总结
- 在 Rust 里,闭包产生的匿名数据类型,格式和 struct 是一样的
- 闭包是存储在栈上,并且除了捕获的数据外,闭包本身不包含任何额外函数指针指向闭包的代码
不同语言的闭包设计
-
大部分编程语言的闭包很多时候无法放在栈上,需要额外的堆分配
-
它们闭包的性能要远低于函数调用
a. 额外的堆内存分配
b. 潜在的动态分派(很多语言会把闭包处理成函数指针)
c. 额外的内存回收
Rust性能和函数差不多的原因
- 如果不使用 move 转移所有权,闭包会引用上下文中的变量,这个引用受借用规则的约束,所以只要编译通过,那么闭包对变量的引用就不会超过变量的生命周期,没有内存安全问题
- 如果使用 move 转移所有权,上下文中的变量在转移后就无法访问,闭包完全接管这些变量,它们的生命周期和闭包一致,所以也不会有内存安全问题
- Rust 为每个闭包生成一个新的类型,又使得调用闭包时可以直接和代码对应,省去了使用函数指针再转一次的额外消耗
Rust 的闭包类型
- 在声明闭包的时候,我们并不需要指定闭包要满足的约束
- 当闭包作为函数的参数或者数据结构的一个域时,我们需要告诉调用者,对闭包的约束
FnOnce
pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
它只能被调用一次
- 一个关联类型 Output,它是 闭包返回值的类型
- 一个方法 call_once,要注意的是 call_once 第一个参数是 self,它会转移 self 的所有权到 call_once 函数中
- FnOnce 的参数,是一个叫 Args 的泛型参数,没有任何约束
简单示例
fn main() {
let name = String::from("Tyr");
// 这个闭包啥也不干,只是把捕获的参数返回去
let c = move |greeting: String| (greeting, name);
// 闭包内的name发生了转移,所以这里闭包变得不完整,无法再次使用
let result = c("hello".to_string());
println!("result: {:?}", result);
// 无法再次调用
let result = c("hi".to_string());
}
- 如果一个闭包并不转移自己的内部数据,那么它就不是 FnOnce
- 一旦它被当做 FnOnce 调用,自己会被转移到 call_once 函数的作用域中,之后就无法再次调用
fn main() {
let name = String::from("Tyr");
// 这个闭包会 clone 内部的数据返回,所以它不是 FnOnce
let c = move |greeting: String| (greeting, name.clone());
// 所以 c1 可以被调用多次
println!("c1 call once: {:?}", c("qiao".into()));
println!("c1 call twice: {:?}", c("bonjour".into()));
// 然而一旦它被当成 FnOnce 被调用,就无法被再次调用
println!("result: {:?}", call_once("hi".into(), c));
// 无法再次调用
// let result = c("hi".to_string());
// fn 也可以被当成 fnOnce 调用,只要接口一致就可以
println!("result: {:?}", call_once("hola".into(), not_closure));
}
fn call_once(arg: String, c: impl FnOnce(String) -> (String, String)) -> (String, String) {
c(arg)
}
fn not_closure(arg: String) -> (String, String) {
(arg, "Rosie".into())
}
/*
c1 call once: ("qiao", "Tyr")
c1 call twice: ("bonjour", "Tyr")
result: ("hi", "Tyr")
result: ("hola", "Rosie")
*/
FnMut
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(
&mut self,
args: Args
) -> Self::Output;
}
- FnMut “继承”了 FnOnce,或者说 FnOnce 是 FnMut 的 super trait
- FnMut 也拥有 Output 这个关联类型和 call_once 这个方法
- 还有一个 call_mut() 方法。注意 call_mut() 传入 &mut self,它不移动 self,所以 FnMut 可以被多次调用
- FnOnce 是 FnMut 的 super trait,所以,一个 FnMut 闭包,可以被传给一个需要 FnOnce 的上下文,此时调用闭包相当于调用了 call_once()
举例
fn main() {
let mut name = String::from("hello");
let mut name1 = String::from("hola");
// name使用了引用
// 如果在闭包 c 里借用了 name,你就不能把 name 移动给另一个闭包 c1
// 捕获 &mut name
let mut c = || {
name.push_str(" Tyr");
println!("c: {}", name);
};
// name1移动了所有权
// 捕获 mut name1,注意 name1 需要声明成 mut
let mut c1 = move || {
name1.push_str("!");
println!("c1: {}", name1);
};
c();
c1();
// FnMut 可以被多次调用,这是因为 call_mut() 使用的是 &mut self,不移动所有权。
call_mut(&mut c);
call_mut(&mut c1);
// c 和 c1 这两个符合 FnMut 的闭包,能作为 FnOnce 来调用
call_once(c);
call_once(c1);
}
// 在作为参数时,FnMut 也要显式地使用 mut,或者 &mut
fn call_mut(c: &mut impl FnMut()) {
c();
}
// 想想看,为啥 call_once 不需要 mut?,因为会移动所有权
fn call_once(c: impl FnOnce()) {
c();
}
/*
c: hello Tyr
c1: hola!
c: hello Tyr Tyr
c1: hola!!
c: hello Tyr Tyr Tyr
c1: hola!!!
*/
Fn
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}
- 它“继承”了 FnMut,或者说 FnMut 是 Fn 的 super trait
- 任何需要 FnOnce 或者 FnMut 的场合,都可以传入满足 Fn 的闭包
举个例子
fn main() {
let v = vec![0u8; 1024];
let v1 = vec![0u8; 1023];
// Fn,不移动所有权
let mut c = |x: u64| v.len() as u64 * x;
// Fn,移动所有权
let mut c1 = move |x: u64| v1.len() as u64 * x;
println!("direct call: {}", c(2));
println!("direct call: {}", c1(2));
println!("call: {}", call(3, &c));
println!("call: {}", call(3, &c1));
println!("call_mut: {}", call_mut(4, &mut c));
println!("call_mut: {}", call_mut(4, &mut c1));
println!("call_once: {}", call_once(5, c));
// 带有move的c1在作为impl FnOnce之后就无法再使用了
println!("call_once: {}", call_once(5, c1));
}
fn call(arg: u64, c: &impl Fn(u64) -> u64) -> u64 {
c(arg)
}
fn call_mut(arg: u64, c: &mut impl FnMut(u64) -> u64) -> u64 {
c(arg)
}
fn call_once(arg: u64, c: impl FnOnce(u64) -> u64) -> u64 {
c(arg)
}
/*
direct call: 2048
direct call: 2046
call: 3072
call: 3069
call_mut: 4096
call_mut: 4092
call_once: 5120
call_once: 5115
*/
闭包使用场景
- thread::spawn
- Iterator中的大部分函数,比如map
fn map<B, F>(self, f: F) -> Map<Self, F>
where
Self: Sized,
F: FnMut(Self::Item) -> B,
{
Map::new(self, f)
}
/*
1. map() 方法接受一个 FnMut,它的参数是 Self::Item,返回值是没有约束的泛型参数 B
2. Self::Item 是 Iterator::next() 方法吐出来的数据,被 map 之后,可以得到另一个结果
*/
- 闭包作为返回值
use std::ops::Mul;
fn main() {
let c1 = curry(5);
println!("5 multiply 2 is: {}", c1(2));
let adder2 = curry(3.14);
println!("pi multiply 4^2 is: {}", adder2(4. * 4.));
}
fn curry<T>(x: T) -> impl Fn(T) -> T
where
T: Mul<Output = T> + Copy,
{
move |y| x * y
}
- 为闭包实现某个trait
pub trait Interceptor {
/// Intercept a request before it is sent, optionally cancelling it.
fn call(&mut self, request: crate::Request<()>) -> Result<crate::Request<()>, Status>;
}
impl<F> Interceptor for F
where
F: FnMut(crate::Request<()>) -> Result<crate::Request<()>, Status>,
{
fn call(&mut self, request: crate::Request<()>) -> Result<crate::Request<()>, Status> {
self(request)
}
}
/*
1. Interceptor 有一个 call 方法,它可以让 gRPC Request 被发送出去之前被修改,一般是添加各种头,比如 Authorization 头
2. 为了让传入的闭包也能通过 Interceptor::call() 来统一调用,可以为符合某个接口的闭包实现 Interceptor trait
*/
小结
闭包的调用效率和函数调用几乎一致
- 闭包捕获的变量,都储存在栈上,没有堆内存分配
- 闭包在创建时会隐式地创建自己的类型,每个闭包都是一个新的类型
- 通过闭包自己唯一的类型,Rust 不需要额外的函数指针来运行闭包
Rust 支持三种不同的闭包 trait:FnOnce、FnMut 和 Fn
- FnOnce 是 FnMut 的 super trait,而 FnMut 又是 Fn 的 super trait
- FnOnce 只能调用一次
- FnMut 允许在执行时修改闭包的内部数据,可以执行多次
- Fn 不允许修改闭包的内部数据,也可以执行多次
链接
- 闭包的定义
- Golang闭包的例子
- C++ lambda分配在堆上的博客
- FnOnce定义
- FnMut
- Fn trait
- Iterator 中的map
- gRPC库 tonic
- (不完全)模拟 FnOnce 中闭包的使用
精选问答
-
下面的代码,闭包 c 相当于一个什么样的结构体?它的长度多大?代码的最后,main() 函数还能访问变量 name 么?为什么?
fn main() { let name = String::from("Tyr"); let vec = vec!["Rust", "Elixir", "Javascript"]; let v = &vec[..]; let data = (1, 2, 3, 4); let c = move || { println!("data: {:?}", data); println!("v: {:?}, name: {:?}", v, name.clone()); }; c(); // 请问在这里,还能访问 name 么?为什么? }a. 相当于以下数据结构
struct Closure<'a, 'b: 'a> { data: (i32, i32, i32, i32), v: &'a [&'b str], name: String, } -
下面的代码,为啥 call_once 不需要 c 是 mut 呢?
// 想想看,为啥 call_once 不需要 mut? fn call_once(mut c: impl FnOnce()) { c(); }a. 调用FnOnce的call_once方法会取得闭包的所有权
b. 对于闭包c和c1来说,即使在声明时不使用mut关键字,也可以在其call_once方法中使用所捕获的变量的可变借用
-
fn和Fn的区别
a. fn 和 Fn / FnMut / FnOnce 不是一回事,fn 是一个 function pointer,不是闭包