捕捉

因为闭包可能被当做返回值,或者通过别的方式离开当前的函数,那闭包里捕捉到的当前函数的变量就可能离开函数的作用域,这样这个变量就不能放在stack上,其他一些有GC的语言通常会把捕捉的变量放到heap,之后再由GC回收。而Rust会分两种情况处理:

  1. 闭包只在当前函数使用,不会被传递出当前的作用域,这时候Rust会自动创建一个捕捉的值的引用。
  2. 如果闭包有可能比当前函数生命周期长的话,捕捉到的变量必须move到闭包里。比如多线程的情况。

    1. fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
    2. -> thread::JoinHandle<Vec<City>>
    3. {
    4. let key_fn = move |city: &City| -> i64 { -city.get_statistic(stat) };
    5. thread::spawn(move || {
    6. cities.sort_by_key(key_fn);
    7. cities
    8. })
    9. }

    闭包里的转移和其他地方一样,Copy类型的会被复制,原变量还可以使用,其他类型就是真的转移了。

函数和闭包的类型

函数的类型:fn(&City) -> i64。函数类型的值是一个指向函数机器码的地址,也就是一个函数指针。闭包的类型不一样,如果要将函数和闭包传递给同一个函数,这个函数需要用泛型Fn(&City) -> bool。Fn泛型是自动实现到所有的函数和大多数闭包的。
闭包需要用泛型是因为,闭包本身没有固定的类型,因为闭包除了有参数和返回值,还有捕获的变量。所以每个闭包都是编译器创建的一个新的类型,这个类型要能装下这些捕捉的变量。但所有的闭包都实现了Fn trait,所以可以和普通函数用在相同的泛型函数。

闭包的性能

Rust的闭包是存储在栈上的,不需要垃圾回收,可以时候inline等一些编译优化,没有其他语言那些over head。

闭包本身的数据结构有点像struct,每个字段就是捕获的变量,如果是捕获的引用,则其字段就是引用,如果是转移的捕获,则其字段就是捕获的值本身。
pr2e_1401.png

闭包和安全

会销毁掉变量所有权的闭包是不能调用两次的,因此这样的闭包实现的是FnOnce trait。
会修改变量的闭包可以调用多次,但不能被传递到另外的线程,这样的闭包是FnMut。
Fn类的函数和闭包是无限制的,可以调用多次也可以传递给其他线程。

Fn是FnMut的subtrait,FnMut是FnOnce的subtrait。FnOnce的限制最多,Fn的限制最少。
Fn是Copy和Clone的,FnMut既不是Copy也不是Clone,对于转移所有权的闭包,如果转移的变量是Copy或Clone那闭包本身就是Copy或Clone。

回调

在定义有回调函数或闭包的数据集合的时候,和trait那章讲的泛型与trait对象一样,使用Fn这样的trait来定义泛型是不行,因为每个闭包虽然有相同的trait但都是不同的类型,是不能放到单一类型的集合里的。需要用trait对象来包装一下。

如果闭包没有捕获任何变量,则他的类型也是一个函数指针fn。这样就可以不用trait object,直接存到Map或者Vec里。