Rust闭包(Closure)有两种捕获变量的方式:
Borrow
: 实际就是一个引用. 闭包的生存周期不会超过捕获的变量生存周期时, 采用这种方式:1
2
3fn sort_by_statistic(cities: &mut Vec<City>, stat: Statistic) {
cities.sort_by_key(|city| -city.get_satistic(stat));
}Steal
(move
): 当闭包的生存周期超过捕获变量时, 需要用关键字move告诉Rust要把使用的变量捕获到闭包中. move的方式有三种:- 如果变量是
Copyable
类型(如i32
), 那么闭包会使用变量的一份拷贝副本. 而在闭包之后, 仍然可以使用这个Copyable
变量. - 如果变量是
NonCopyable
(如Vec<City>
), 那么闭包会攫取(steal)变量的所有权, 真正意义上的move到闭包中. 闭包之后也不能再使用该变量. - 如果闭包后仍然要使用
NonCopyable
的变量, 那么可以使用clone
方法, 深拷贝出一份副本供闭包使用.
- 如果变量是
Function & Closure Types
和C/C++一样, 函数名就像一个函数指针, 指向函数的机器码.
闭包和函数的类型不同
fn
type的参数只能接受函数指针Fn
trait既可以接受函数指针, 也能接受闭包. 实际上每个闭包都实现了Fn
trait(泛指下面的Fn/FnOnce/FnMut
之一).1
2
3
4
5
6
7
8// 只能接受函数指针
fn count_selected_cities(cities: &Vec<City>, test_fn: fn(&City) -> bool) -> usize {
...
}
// 既能接受函数指针, 也能接受闭包
fn count_selected_cities(cities: &Vec<City>, test_fn: F) -> usize
where F: Fn(&City) -> bool { ... }
每个闭包都有自己临时的唯一类型, 没有任意两个闭包会有相同的Type
Closure性能
大部分语言把闭包创建在堆上(由GC回收), 调用的时候有一些额外的成本, 更重要的是: 这样完全无法利用
inline
提升性能.Rust没有这方面的妥协. 除非显式的把闭包创建在Box, Vec或其它容器中, Rust不会把闭包放在堆上. 而且因为每个闭包都有它自己的类型, Rust也能通过捕获的方式知道需要开辟多大的空间给变量, 从而可以把闭包做inline优化. 如下图:
![Closure Memory Model](/images/Screen Shot 2019-09-04 at 01.08.26.png)
(a)
: 按引用捕获(Borrow
), 实际只需要两个指针大小指向变量.(b)
: 以move
的方式攫取(Steal)变量, 存放两个拷贝.(c)
: 不需要捕获任何变量, 0成本!
无论哪种形式的闭包, 没有多余的开销, 连一个指向闭包的指针都没有!
即使在堆栈上的闭包(下面会说), 其调用性能也和Trait里的方法调用性能差不多. 意思就是比C++的虚函数指针快.
Fn, FnMut & FnOnce
闭包内会销毁(drop)
Borrow
变量的不能二次调用, 否则引发多次销毁, Rust会在编译时期检测到这点.1
2
3
4
5let my_str = "hello".to_string();
let f = || drop(my_str);
f(); // ok
f(); // error: use of moved value针对上面的情况, 下面的代码也会报错:
1
2
3
4
5
6
7
8
9fn call_twice<F>(closure: F) where F: Fn() {
closure();
closure();
}
let my_str = "hello".to_string();
let f = || drop(my_str);
call_twice(f); // expoected a closure that implements the `Fn` trait,
// but this closure only implements `FnOnce`编译器提示
f
这个闭包对象实际上是个FnOnce
, 它和Fn
以及FnMut
的声明如下:1
2
3
4
5
6
7
8
9
10
11
12pub trait FnOnce<Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
}
pub trait FnMut<Args>: FnOnce<Args> {
extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
pub trait Fn<Args>: FnMut<Args> {
extern "rust-call" fn call(&self, args: Args) -> Self::Output;
}可以看到这三个类型的父子trait关系. 在
call_twice
这个函数内,closure
其实会被扩展成:closure.call()
, 但是给定的f
只允许closure.call_once()
, 无法在编译时期扩展这个generic, 所以报错.FnOnce
的参数是self
, 所以调用call_once
后, 这个闭包也就用尽销毁了(used up). 要避免这个问题的话可以改为捕获变量的引用(这么改后就要注意保证引用变量的生命周期了):1
2
3
4
5
6let dict = produce_dict();
let debug_dum_dict = || {
for (key, value) in &dict { // does not use up dict
println!("{:?} - {:?}", key, value);
}
};FnMut
的参数是&mut self
, 所以允许以mut reference
的形式捕获并修改变量. 成为FnMut
有两个必要条件:- mut access a value
- doesn’t drop any value
Callback
我们通常会把闭包做为回调保存起来, 等到触发的时候调用, 比如:
1 | struct BasicRouter<C> where C: Fn(&Request) -> Response { |
这里报错, 因为没有每个闭包都有自己唯一的类型, 所以这个
HashMap
无法扩展.解决方法, 用
Box
去封装一次, 使Box<Fn(&Request)>
成为一个指向堆上的闭包的指针, 也就是其它语言中常说的回调.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18type BoxedCallback = Box<Fn(&Request) -> Response>;
struct BasicRouter {
routes: HashMap<String, BoxedCallback>
}
impl BasicRouter {
/// Create an empty router.
fn new() {
BasicRouter { routes: HashMap::new() }
}
fn add_route<C>(&mut self, url: &str, callback: C)
where C: Fn(&Request) -> Response + 'static
{
self.routes.insert(url.to_string(), callback);
}
}这里加
'static
是为了保证闭包Borrow
的变量引用在离开scope后仍然安全.