【Rust 笔记】12-闭包

12 - 闭包

  • 排序整数:

    integers.sort();
    
  • 闭包:即匿名函数表达式,可以用来排序复合类型:

    struct City {
          
          
      name: String,
      population: i64,
      country: String,
      ...
    }
    
    /// 按人口排序城市的辅助函数
    // 接收City记录,然后提取键(Key)
    fn city_population_descending(city: &City) -> i64 {
          
          
      -city.population
    }
    // sort_by_key将键函数作为参数
    fn sort_cities(cities: &mut Vec<City>) {
          
          
        cities.sort_by_key(city_population_descending);
    }
    
    // 上述两个函数通过闭包简写为如下所示
    fn sort_cities(cities: &mut Vec<City>) {
          
          
      cities.sort_by_key(|city| -city.population)
      // |city| -city.population接收一个参数city,返回-city.polulation。
    }
    
  • 标准库中可接收闭包的特性:

    • Iteratormapfilter 方法,用于操作顺序数据
    • 启动新系统线程的 thread::spawn 等线程 API。并发的核心是在线程间交换工作,而闭包可以方便地表示工作单元。
    • 某些方法会在必要时计算默认值,如 HashMapor_insert_with 方法。默认值以闭包形式传入,只会在必须创建新值时调用。

12.1 - 捕获变量

  • 闭包可以使用属于包含函数的数据:

    /// 按几个不同的统计指标排序
    fn sort_by_statisics(citeis: &mut Vec<City>, stat: Statistic) {
          
          
      cities.sort_by_key(|city| -city.get_statistic(stat));  // 闭包捕获了stat
    }
    
  • 大多数支持闭包的语言,需要密切结合垃圾回收,如下面的 JavaScript 代码:

    // 启动重排城市表格行的动画
    function startSortingAnimation(cities, stat) {
          
          
      // 用于排序表格的辅助函数
      // 注意,这个函数引用了stat
      function keyfn(city) {
          
          
        return city.get_statistic(stat);
      }
    
      if (pendingSort)
          pendingSort.cancel();
    
      // 现在启动动画,传入keyfn
      // 后面排序算法会调用keyfn
      pendingSort = new SortingAnimation(cities, keyfn);
    }
    
  • Rust 没有垃圾回收,如何实现这个特性?

12.1.1 - 借用值的闭包

  • 闭包遵循借用和生命期规则。

  • 在 12.1 的例子中,因为闭包包含对

    stat
    

    的引用,所以 Rust 不会让闭包的存活期超过

    stat
    

    。闭包只在排序期间使用。

    • 此处 stat 会被保存在栈上。
    • 相对 GC 分配会比较快。
  • Rust 使用生命期来确保代码安全,而不是垃圾回收。

12.1.2 - 盗用值的闭包

  • 如下例子:

    use std::thread;
    
    fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
        -> thread::JoinHandle<Vec<City>>  
        // 闭包的返回值会包装在JoinHandle中返回给调用线程
    {
          
          
        let key_fn = |city: &City| -> i64 {
          
           -city.get_statistic(stat) };
    
        thread::spawn(|| {
          
            // `||`表示闭包没有参数。
            cities.sort_by_key(key_fn);
            cities
        })
    }
    
    • thread::spawn 接收一个闭包,并在新的系统线程中调用这个闭包。新线程与调用程序并行运行。闭包返回后,新线程退出。

    • 闭包 key_fn 包含对 stat 的引用。但此时 Rust 会拒绝这个程序,并提示以下信息:

      问题1: `|city: &City| -> i64`   // `stat` is borrowed here
      问题2(stat)                  // may outlive borrowed value `stat`
      
    • cities 属于不安全共享,thread::spawn 创建的新线程不能保证自己在 citiesstat 被销毁(函数结束)前完成任务

    • 解决方案:让 Rust 把 citiesstat 转移到使用他们的闭包中,而不要再引用他们。

      fn start_sorting_thread(mut cities: Vec<City>, stat: Statistic)
          -> thread::JoinHandle<Vec<City>>
      {
              
              
          let key_fn = move |city: &City| -> i64 {
              
               -city.get_statistic(stat) };
          // key_fn获得了stat的所有权
      
          thread::spawn(move || {
              
                // 获得了cities和key_fn的所有权
              cities.sort_by_key(key_fn);
              cities
          })
      }
      
    • 在两个闭包前面加了 move 关键字:告诉 Rust,这个闭包不是在借用它使用的变量,而是要把它偷走。

  • Rust 为闭包提供了两种从包含函数取得数据的方式:转移和借用。—— 也是保证线程安全的方式

    • 如果闭包转移了一个可复制的值(i32),那么它会复制该值。因此,上述代码中 Statistic 恰好是一个可复制类型,那么在创建使用它的转移闭包后,同样还可以使用 stat
    • Vec<City> 这样不可复制类型,才会真正转移。上述代码通过转移闭包,把 cities 转移到新线程中。在创建这个闭包的代码后面,Rust 不会再让其他人访问 cities
    • 上述代码在闭包转移 cities 后,不需要再使用 cities 了。如果还需要使用,那么可以告诉 Rust 克隆 cities 并将副本保存到另一个变量中。闭包只能偷走它自己引用的那个副本。

12.2 - 函数与闭包类型

  • 函数和闭包可以当成值来使用,自然它们也有自己的类型。

  • Rust 的函数值本质上就是指针:

    • 下述函数的类型是 fn(&City) -> i64:表示接收一个参数(&City),返回一个 i64 值。

      fn city_population_descending(city: &City) -> i64 {
              
              
          -city.population
      }
      
    • 可以像使用其他值一样使用函数。即可以把函数保存在变量里,也可以使用常用的 Rust 语法计算函数值:

      let my_key_fn: fn(&City) -> i64 = 
          if user.prefs.by_population {
              
              
              city_population_descending
          } else {
              
              
              city_monster_attack_risk_descending
          };
      cities.sort_by_key(my_key_fn);
      
    • 结构体可以包含函数类型的字段。

    • 泛型类型如 Vec 可以存储一批函数,只要它们的 fn 类型一样就可以。

    • 函数值很小,一个 fn 值就是这个函数机器码的内存地址,与 C++ 中的函数指针类似。

    • 一个函数可以接收另一个函数作为参数:

      /// 传入一组城市和一个测试函数
      /// 返回满足条件的所有城市
      fn count_selected_cities(cities: &Vec<City>, 
          test_fn: fn(&City) -> bool) -> usize
      {
              
              
          let mut count = 0;
          for city in cities {
              
              
              if test_fn(city) {
              
              
                  count += 1;
              }
          }
          count
      }
      
      /// 测试函数举例。
      /// 这个函数的类型是:fn(&City) -> bool
      /// 跟count_selected_cities函数的参数test_fn的类型一致
      fn has_monster_attacks(city: &city) -> bool {
              
              
          city.monster_attack_risk > 0.0
      }
      
      // 计算城市被袭击的风险
      let n = count_selected_cities(&my_cities, has_monster_attacks);
      
  • 闭包与函数不是同一种类型,每个闭包都有自己的类型,在使用闭包的代码中通常需要是泛型的。

    • 如下所示的例子:

      let limit = preferences.acceptable_monster_risk();
      let n = count_selected_cities(
          &my_cities,
          |city| city.monster_attack_risk > limit  // 类型错误:类型不匹配
      );
      
    • 针对上述类型错误,必须修改函数的类型签名:

      fn count_selected_cities<F>(cities: &Vec<City>, test_fn: F) -> usize
          where F: Fn(&City) -> bool
      {
              
              
          let mut count = 0;
      
          for city in cities {
              
              
              if test_fn(city) {
              
              
                  count += 1;
              }
          }
          count
      }
      
    • 新版本的 count_selected_cities 函数是一个泛型的,接收任意类型 F 的参数 test_fnF 必须实现特型 Fn(&City) -> bool。所有以一个 &City 为参数且返回博而至的函数和闭包都会自动实现这个特型:

      • -> 和后面的返回值类型是可选的。
      • 如果省略,那么返回值类型为 ()
      fn(&City) -> bool   // fn类型(仅函数)
      Fn(&City) -> bool   // Fn特型(包括函数和闭包)
      
    • 新版本的 count_selected_cities 可以接收函数,也可以接收闭包:

      count_selected_cities(
          &my_cities,
          has_moster_attacks
      );
      count_selected_cities(
          &my_cities,
          |city| city.monster_attack_risk > limit
      );
      
    • 虽然闭包可以调用,但它不是 fn 类型。闭包 |city| city.monster_attack_risk > limit 有自己的类型。这种类型通常是一个临时类型,大到足以存储它的数据。

  • 任何两个闭包的类型都不相同。

    • 所有的闭包都会实现 Fn 特型。
    • Fn(&City) -> bool

12.3 - 闭包的性能

  • 大多数编程语言的闭包通常是分配在堆上,动态分派,然后由垃圾回收程序负责回收。编译器很难对闭包进行行内化优化策略,以减少函数调用并进而引用其他优化。这样的闭包通常会拖慢内部循环(tight inner loop)的性能。

  • Rust 的闭包通常不会被分配在堆上,除非把闭包封装到 BoxVec 或其他容器里。每个闭包都有不同的类型,Rust 编译器只需要知道所调用闭包的类型,就可以将该闭包的代码行内化。所以,Rust 的闭包支持在内部循环中使用。

  • 下面的例子中,闭包会引用两个局部变量:字符串 food 和值为 27 的简单枚举 weather

    let food = "tacos";
    let weather = Weather::Tornadoes;
    |city| city.eats(food) && city.has(weather)        // a、第一次使用闭包
    move |city| city.eats(food) && city.has(weather)   // b、第二次使用闭包
    |city| city.eats("crawfish")                       // c、第三次使用闭包
    
    • 闭包 a、在内存中,这个闭包类似一个小结构体,其包含对他所引用变量的引用。
    • 闭包 b、与上相同,不过它是一个转移闭包,因此会包含实际的值,而非引用。
    • 闭包 c、没有用到起环境中的任何变量。此时结构体是空的,因此这个闭包根本不会占用内存。

12.4 - 闭包和安全 —— 在堆上分配闭包的方法

闭包主要在创建的时候可能转移或借用被捕获的变量。所造成的影响十分不明显,特别是在闭包清除或修改捕获的值时。

12.4.1 - 杀值的闭包

  • 杀值,即清除(drop)值,最直观的方式是调用 drop()

    let my_str = "hello".to_string();
    let f = || drop(my_str);
    
    • 如果调用 f 两次:

      f();
      f();
      
    • 第一次调用 fmy_str 会被清除,意味着会释放存储字符串的内存,交还给系统。

    • 第二次调用 f,同样的操作又执行了一边。这就是 C++ 中会触发未定义行为的经典错误:重复释放(double free)。

  • 而在 Rust 中,编译时检查可以发现上述错误。

  • 一个闭包只能被调用一次。

  • 闭包必须严格遵守生命期规则,即在调用时,值会被用尽(即转移)。

12.4.2-FnOnce

  • 尝试欺骗 Rust,让它两次清除一个 String。构造如下泛型函数:

    fn call_twice<F>(closure: F) where F: Fn() {
        closure();
        closure()
    }
    
    • 可以给这个函数传入任何实现 Fn 特型的闭包。这样的闭包不接收参数,且会返回 ()

    • 把 12.4.1 中那个不安全闭包的闭包传入:

      let my_str = "hello".to_string();
      let f = || drop(my_str);
      call_twice(f);
      
    • 在编译时,Rust 会报以下错误:

      error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
       --> closures_twice.rs:12:13
         |
      12 |    let f = || drop(my_str);
         |            ^^^^^^^^^^^^^^^
         |
      note: the requirement to implement `Fn` derives from here
       --> closures_twice.rs:13:5
         |
      13 |    call_twice(f);
         |    ^^^^^^^^^^
      
    • 上述错误体现了 Rust 是如何处理 “杀值闭包” 的。即从语言层面上被完全禁止。

  • f 这样的闭包不能是 Fn,而是不通用的特型 FnOnce,这种特型的闭包只能调用一次。

    • 第一次调用 FnOnce 闭包时,闭包本身也会被用掉。

    • 如下所示,两个特型 FnFnOnce 的定义:

      // 伪代码,没有参数
      
      /// 对于`Fn`闭包,`closure()`方法会扩展为`closure().call()`,这个方法以自身的引用作为参数,因此这个闭包不会被转移。
      trait Fn() -> R {
              
              
          fn call(&self) -R;
      }
      
      /// 但是如果闭包只能安全地调用一次,即对于`FnOnce`闭包,`closure()`方法会被扩展为`closure().call_once()`,这个方法会取得`self`的值,所以闭包会被用掉。
      trait FnOnce() -R {
              
              
          fn call_once(self) -> R;
      }
      
  • 如下常见错误,在实际开发中需要注意:

    • 直接迭代 dict,导致它被用掉了。使得该闭包成了 FnOnce 类型。

      let dict = produce_glossary();
      let debug_dump_dict = || {
              
              
          for (key, value) in dict {
              
              
              println!("{:?} - {:?}", key, value);
          }
      };
      
    • 应该改为 &dict,而不是 dict,即要访问值的引用。让这个闭包称为 Fn 类型。

      let debug_dump_dict = || {
              
              
          for (key, value) in &dict {
              
              
              println!("{:?} - {:?}", key, value);
          }
      };
      

12.4.3-FnMut

  • Rust 认为非 mut 值可以安全地在线程间共享。

    • 但是如果共享的非 mut 闭包里包含了 mut 数据,同样是不安全的。
    • 在多个线程里调用这个闭包,会导致各种资源争用问题,与多个线程同时读写相同的数据一样。
  • FnMut 类型的闭包:包含可修改数据或 mut 引用的闭包。

    • 即可以写数据的闭包。

    • FnMut 闭包要使用 mut 引用来调用。

    • 定义:

      // 特型定义为伪代码实现
      
      /// Fn是没有调用次数限制的闭包和函数,是所有fn函数中最高的一种。
      trait Fn() -> R {
              
              
          fn call(&self) -R;
      }
      
      /// FnMut是如果闭包本身声明为mut,也可以多次调用的闭包。
      trait FnMut() -R {
              
              
          fn call_mut(&mut self) -R;
      }
      
      /// FnOnce是如果调用者拥有闭包,则只能调用一次。
      trait FnOnce() -R {
              
              
          fn call_once(self) -R;
      }
      
  • 任何需要以 mut 方式访问值,但不会清除任何值的闭包都是 FnMut 闭包。

    let mut i = 0;
    let incr = || {
          
             // incr是FnMut,而不是Fn
        i += 1;    // 借用对i的可修改引用
        println!("Ding! i is now: {}", i);
    };
    call_twice(incr);
    
  • 每个 Fn 都满足 FnMut 的要求,每个 FnMut 都满足 FnOnce 的要求。

    • Fn()FnMut() 的子特型;
    • FnMut()FnOnce() 的子特型。
  • call_twice 函数应该接收所有 FnMut 闭包,即应该修改为:

    fn call_twice<F>(mut closure: F) where F: FnMut {
          
          
        closure();
        closure();
    }
    
    • 绑定由 F: Fn(),修改为 F: FnMut()。这样仍然可以接收所有 Fn 闭包。

    • 此时可以对修改数据的闭包调用新的 call_twice 函数。

      let mut i = 0;
      call_twice(|| i += 1);
      assert_eq!(i, 2);
      

12.5 - 回调

  • 回调:用户提供的函数,供库在以后调用。

  • 以 Icon 框架举例:

    type BoxedCallback = Box<Fn(&Request) -> Response>;
    struct BasicROuter {
          
          
        routes: HashMap<String, BoxedCallback>
    }
    
    • 每个箱子可以包含不通类型的闭包。

    • 一个 HashMap 可以包含所有类型的回调。

    • 调整相应的方法:

      impl BasicRouter {
              
              
          /// 创建一个空路由器
          fn new() -> BasicRouter {
              
              
              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(), Box::new(callback));
          }
      }
      
    • 处理请求:

      impl BasicRouter {
              
              
          fn handle_request(&self, request: &Request) -> Response {
              
              
              match self.routes.get(&request.url) {
              
              
                  None => not_found_response(),
                  Some(callback) => callback(request)
              }
          }
      }
      

12.6 - 有效使用闭包

  • 在 MVC(Model—View—Controller,模型 — 视图 — 控制器)设计模式中,对用户界面上的每个元素,MVC 框架都会创建 3 个对象:模型、视图和控制器。模型表示 UI 元素的状态;视图负责元素的外观;控制器处理用户交互。

    • 每个对象都会有另一个或另两个对象的引用。可能是直接引用,也可能是通过回调来引用。
    • 在 3 个对象中的一个对象发生了某个事件时,它会通知另外两个对象,因此一切会立即更新。
    • 但是,哪个对象拥有其他对象则无法区分。
  • 在 Rust 中,必须明确所有权,必须消除循环引用。模型和控制器不能直接相互引用。

    • 可以让每个闭包接收它需要的引用作为参数,通过闭包所有权和生命期来解决问题。

    • 可以在系统中为每件东西分配一个数值,然后传递数值而不传递引用。

    • 可以实现诸多 MVC 变体中的一种,保证对象之间并不是都相互引用。

    • 可以仿效某个 非 MVC 系统,比如 Fackbook 的 Flux 架构,实现单向数据流。

      from user input -> Action -> Dispatcher -> Store -> View -> to disblay
      
  • 迭代器是闭包真正大显身手的主题。

    • 可以利用 Rust 闭包的简洁、速度和高效写出不同风格的代码。

详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十四章
原文地址

猜你喜欢

转载自blog.csdn.net/feiyanaffection/article/details/125574821
今日推荐