函数指针指的是形如这样的一种类型:fn(&T) -> i64,圆括号里是参数的类型,如果没有返回值(返回值为 ())就没有 -> 后面的那些东西,反之 -> 后面接返回值类型。
函数指针的大小为 usize,在底层就表示指向函数对应机器码位置的指针。
闭包的定义形如这样:|a: i32, b: i32| -> i32 { ... },和函数的定义比较像,可以理解为是一种匿名函数。
闭包的一个特点是可以捕获外部的值,而普通的函数只能使用传递进来的参数:
rust1234fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|key| -city.get_statistic(stat));
// 上面这个闭包就捕获了外部的 `stat`
}
闭包捕获值的时候会有三种捕获方式:取不可变引用、取可变引用,取所有权。
默认情况下闭包会根据外部的值在闭包内的使用方式自动决定采取什么样的捕获方法。比如上面的代码中,city.get_statistic 的参数是取引用的话,这个闭包就是按取不可变引用的方式捕获 stat。
rust123let my_str = "hello".to_string();
let f = || drop(my_str);
// 这里这个闭包就是以取所有权的方式捕获 `my_str`
闭包以引用的形式捕获外部的值之后,在闭包内部使用时,会出现类似自动解引用的情况:
rust12345678let mut n = 0;
let mut f = || {
// 这里实际上是以可变引用的方式捕获了 n
// 但是这里的 n 并不是 &mut i32,相当于自动给解引用了
// 但是还是可以将 n 视为引用去传递
n += 1;
n
};
使用 move 关键字可以强制闭包一律以取所有权的方式捕获。
在底层,闭包只存储其捕获的值:
qq_pic_merged_1776860464561如果按引用捕获,则存储被捕获的值的引用,如果按所有权捕获,就直接存储被捕获的值本身。如果什么都不捕获,那么闭包本身甚至不占用任何空间。
闭包在底层并没有存储函数指针。
闭包的生命周期为其捕获的所有引用的生命周期的最小值。很多地方给闭包参数加上了 'static 限制,意思是要求闭包本身不能捕获任何的引用(除非是 'static 的引用)。很多类似线程 spawn 的场景往往就需要给闭包加上 move 修饰,确保闭包没有按引用捕获外部的值,从而闭包本身的生命周期是 'static 的。
如果闭包以所有权形式捕获的所有值,以及以引用形式捕获的所有引用,都是 Copy 的,则闭包是 Copy 的,如果都是 Clone 的,则闭包是 Clone 的(注意,不可变引用是 Copy 的,可变引用既不是 Copy 也不是 Clone)
即使参数和返回值类型都一样的闭包,可能捕获的值的个数/类型之类的不一样,所以每个闭包都有自己的类型,不存在两个类型一样的闭包。
特别地,如果一个闭包不捕获任何东西,那么这个闭包可以视为函数指针类型:
rust1let a: fn(u32) -> u32 = |x| x + 1; // ok
但是我们可以说,参数和返回值类型都一样的闭包,都实现了某个 trait。
和闭包有关的 trait 有三种:
FnOnce:如 FnOnce(u32) -> u32,会消耗掉捕获进来的值的所有权的闭包就会被判定为 FnOnce。
调用仅实现 FnOnce 的闭包相当于是直接消费掉了闭包的所有权,即 call(self),调用一次后就不能再调用了。
FnMut:会修改捕获进来的值的闭包就会被判定为 FnMut。
调用仅实现 FnOnce 的闭包相当于 call(&mut self)
Fn:不会消耗所有权也不会进行可变引用的闭包
调用仅实现 Fn 的闭包相当于 call(&self)
我们应该把闭包本身理解为其捕获内容的集合(和上面的内存结构对应),call(&self),call(&mut self) 和 call(self) 中的 self 指的是闭包捕获的内容。
比如:
rust123456789let mut n = 1;
let mut f = move || { // 把 n 的所有权移进来了
n += 1;
n
};
f(); // 返回 1
// 但是这里外层的 n 为 1
// 因为闭包是按所有权捕获的 n,但是 n 是 Copy 的,因此相当于闭包内部维护了一个 n 的副本
f(); // 返回 2,意味着闭包一经定义就完成了捕获,后续调用都是基于定义时捕获的内容的
同时值得注意的是,尽管这里转移了所有权,但是 f 被判定为 FnMut,因为 f 仅修改了自己捕获的内容,而没有消费掉自己捕获的内容。
同时上述三种 trait 有一种集合关系:
从接受的角度来看,FnOnce 表示最为宽松的限制,所有闭包都可以满足。Fn 表示最紧的限制。从使用的角度来说,FnOnce 表示最紧的限制,Fn 表示最松的限制。
比如说,有一个函数的参数接收 FnMut,你把 Fn 传过去是没问题的。因为参数接收 FnMut 意味着调用闭包的时候会给到被捕获内容的可变引用,而你的闭包本身是 Fn 则意味着闭包被调用后只使用到被捕获内容的不可变引用,用不到可变引用。
直接调用闭包的话,就跟自己写一个结构体并 impl 一个函数再调用差不多:
rust1234567891011let x = 10;
let f = |y| x + y;
let r = f(5);
// 等价于:
struct MyClosure { x: i32 }
impl MyClosure {
fn call(&self, y: i32) -> i32 { self.x + y }
}
let f = MyClosure { x: 10 };
let r = f.call(5);
如果是要传递闭包的话,那么闭包的开销等价于 Trait 的开销。如果是静态分发的话,不仅是零成本的,而且编译器还可能直接把函数内联掉。
async { ... } 表示一个 Async Block,整个 Async Block 表示一个 Future。
和 Future 一样,如果没有被 Poll 的话,Future 内的代码不会执行。
AsyncBlock 的捕获规则和闭包一样,都是按需捕获。async move { ... } 会全部按所有权捕获外部的值。
和闭包不同的是,AsyncBlock 不能指定返回值类型,所以需要一些 workaround:
rust12345let future = async {
...
Ok::<(), std::io::Error>(())
// 提示编译器,这个 AsyncBlock 的返回值是 Result<(), std::io::Error>
}
async fn 可以等价于返回一个 AsyncBlock 的 fn。
Async 闭包有两种写法:
普通闭包+返回 Future
我们可以写 |...| async move { ... } 或是 |...| async { ... } ,其对应类型相当于 impl Fn() -> impl Future<OutPut = T> 。
注意,Rust 中闭包的返回值是不能包含对闭包捕获的值的引用的:
rust1234trait FnMut<Args> {
type Output;
fn call_mut(&mut self, args: Args) -> Self::Output;
}
FnMut 的返回值类型是直接写在 Trait 的关联类型上的,Output 这个类型本身的生命周期和 call_mut 中的 self 的生命周期无关,于是返回值的生命周期无法和闭包本身被捕获的值的生命周期建立起联系。
有的时候 Rust 会把外面的闭包的捕获方式判定为按所有权捕获,但是内部的 AsyncBlock 按引用捕获闭包已经捕获的内容,这就形成了返回值引用闭包内的内容的情况,造成编译错误。所以很多时候我们写 async move 使得 AsyncBlock 不按引用捕获,就不会造成这种问题。
如果闭包是按引用捕获外面的值的话,由于引用是 Copy 的,所以这个引用可以直接被传入 AsyncBlock 中,同时 Rust 会判定这个 AsyncBlock 是引用的外部的值,其生命周期和外部的生命周期一致,而和闭包的生命周期无关,因此这是没问题的。
同时,Rust 中闭包的参数中的引用的生命周期很可能会被推断为高阶生命周期,比如:
rust123let f = |x: &str| {
println!("{}", x);
};
上述闭包会被 Rust 推断成:for<'a> Fn(&'a str) -> () 这个类型,即闭包能够处理具有任何的生命周期的 x。但是如果是返回 AsyncBlock,则:
rust123let f = |text: &str| async {
println!("{}", text);
};
编译器会将闭包的类型推导为 for<'a> Fn(&'a str) -> (impl Future<Output = ()> + 'a),但是回忆闭包的返回值类型 Output 是一个具体的类型,其类型不能随着参数生命周期的变化而变化,因此编译不过。
这意味着这种 Async 闭包写法甚至无法使用参数中的引用。
同理,如果闭包的参数是接收所有权而不是引用的话,返回的 AsyncBlock 也是不能引用这个参数的。
原生 Async 闭包
async |...| {...} 或是 async move |...| {...} 。这种闭包和普通的闭包一样,也对应于三个 Trait:AsyncFnOnce、AsyncFnMut、AsyncFn,但是其定义和普通闭包略有不同:
rust1234567trait AsyncFnMut<Args>: AsyncFnOnce<Args> {
type CallRefFuture<'a>: Future<Output = Self::Output>
where
Self: 'a;
fn call_mut(&mut self, args: Args) -> Self::CallRefFuture<'_>;
}
其使用了 GAT,即在关联类型上面添加与 Self 相同的生命周期,这样闭包调用的返回值就具有了和闭包本身同样的生命周期,就不存在上述那种闭包的问题。