Rust入门失败之Closure

  • Rust闭包(Closure)有两种捕获变量的方式:

    • Borrow: 实际就是一个引用. 闭包的生存周期不会超过捕获的变量生存周期时, 采用这种方式:

      1
      2
      3
      fn 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++一样, 函数名就像一个函数指针, 指向函数的机器码.

  • 闭包和函数的类型不同

    • fntype的参数只能接受函数指针

    • Fntrait既可以接受函数指针, 也能接受闭包. 实际上每个闭包都实现了Fntrait(泛指下面的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

    1. (a): 按引用捕获(Borrow), 实际只需要两个指针大小指向变量.
    2. (b): 以move的方式攫取(Steal)变量, 存放两个拷贝.
    3. (c): 不需要捕获任何变量, 0成本!
  • 无论哪种形式的闭包, 没有多余的开销, 连一个指向闭包的指针都没有!

  • 即使在堆栈上的闭包(下面会说), 其调用性能也和Trait里的方法调用性能差不多. 意思就是比C++的虚函数指针快.

Fn, FnMut & FnOnce

  • 闭包内会销毁(drop)Borrow变量的不能二次调用, 否则引发多次销毁, Rust会在编译时期检测到这点.

    1
    2
    3
    4
    5
    let 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
    9
    fn 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
    12
    pub 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
    6
    let 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct BasicRouter<C> where C: Fn(&Request) -> Response {
routes: HashMap<String, C>
}

impl<C> BasicRouter<C> where C: Fn(&Request) -> Response {
/// Create an empty router.
fn new() -> BasicRouter<C> {
BasicRouter { routes: HashMap::new() }
}

fn add_route(&mut self, url: &str, callback: C) {
self.routes.insert(url.to_string(), callback);
}
}

let mut router = BasicRouter::new();
router.add_route("/", |_| get_form_response());
// mismatched type
// note: no two closures, even if identical, have the same type
  • 这里报错, 因为没有每个闭包都有自己唯一的类型, 所以这个HashMap无法扩展.

  • 解决方法, 用Box去封装一次, 使Box<Fn(&Request)>成为一个指向堆上的闭包的指针, 也就是其它语言中常说的回调.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type 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后仍然安全.