暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

写给 Web 开发者的 Rust 语言入门教程(16.闭包)

最后的叶子的奇妙小屋 2021-07-27
264

之前的文章:

0.引言

1.模块基础

2.变量

3.字符串

4.分支循环

5.数据表示

6.模块化

7.接口抽象

8.泛型

9.错误处理

10.内存布局

11.所有权

12.引用传递

13.引用字段

14.引用计数

15.泛化特化

本章主要介绍 rust 的闭包,包括与闭包相关的可变性、生命周期参数等问题。

闭包基础语法

与其他编程语言中的概念一致,“闭包”指的是一种简单的函数,它可以嵌套地写在其他函数中,并访问所在函数中的变量。在 rust 中的闭包基本语法如下例所示:

    // 创建一个闭包
    let f = | /* 参数列表 */ | {
        // 代码
    };

    多数情况下,闭包是传递给其他函数,作为“回调函数”来使用的。在传递闭包时,可以用一种特殊的参数类型 FnMut(...) 标示。例如:

      fn for_each_item(
      v: &Vec<u32>,
          // 下面这个参数是闭包类型的
          // 它接收一个 u32 类型的参数
          // 且没有返回值
          mut f: impl FnMut(u32),
      ) {
      for item in v {
      f(*item);
      }
      }


      fn main() {
      let v = vec![1, 3, 5, 7];
      let mut sum = 0;
      // 将一个闭包传递给这个函数
      // 闭包的参数类型和返回值类型可以省略
      for_each_item(&v, |item| {
      sum += item;
      });
      println!("{}", sum); // 输出 16
      }

      闭包类型

      具体而言,闭包的类型定义分为 FnOnce(...) FnMut(...) Fn(...) 三种。

      FnOnce(...) 用于只被调用一次的闭包。在这样的闭包中,可以将闭包外变量的所有权转移进闭包内。例如:

        struct MyStruct {
        n: u32,
        }


        fn for_the_first_item(
        v: &Vec<u32>,
        // 这里接受 FnOnce 类型的闭包
        f: impl FnOnce(u32),
        ) {
        if v.len() >= 1 {
        f(v[0]);
        }
        }


        fn main() {
            let v = vec![2357];
        let s = MyStruct {
        n: 4,
        };


        let mut prod = 0;
        for_the_first_item(&v, |item| {
        // 对于 FnOnce 的闭包,
        // 可以将闭包外变量的所有权转移进来
        let t = s;
        prod = t.n * item;
            });


        println!("{}", prod); // 输出 8
        }

        FnMut(...) 最为常见,是可以调用多次的闭包。在这样的闭包中,可以正常访问闭包外变量,但不能将闭包外变量的所有权转移进闭包内(因为闭包可能被执行多次,但所有权是唯一的)。

          struct MyStruct {
          n: u32,
          }


          fn for_each_item(
          v: &Vec<u32>,
          // 这里接受 FnOnce 类型的闭包
          mut f: impl FnMut(u32),
          ) {
          for item in v {
          f(*item);
          }
          }


          fn main() {
          let v = vec![2, 3, 5, 7];
          let s = MyStruct {
          n: 4,
          };


          let mut prod = 0;
          for_each_item(&v, |item| {
          // 对于 FnMut 的闭包,
          // 可以访问闭包外的变量但不能转移所有权,
          // 因而通常使用引用访问的方式
          let t = &s;
          prod += t.n * item;
          });


          println!("{}", prod); // 输出 68
          }

          Fn(...) 是要求最为严格的闭包。与 FnMut(...) 的区别是,它只能不可变地访问闭包外变量,不能改变闭包外变量。通常,这样的闭包是“无状态的”,即调用时传递同样的参数就必然得到同样的结果。

            struct MyStruct {
            n: u32,
            }


            fn for_each_item(
            v: &Vec<u32>,
            // 这里接受 Fn 类型的闭包
            // 这个闭包具有 u32 类型的返回值
            f: impl Fn(u32) -> u32,
            ) -> u32 {
            let mut sum = 0;
            for item in v {
            sum += f(*item);
            }
            sum
            }


            fn main() {
            let v = vec![2, 3, 5, 7];
            let s = MyStruct {
            n: 4,
            };


            let prod = for_each_item(&v, |item| {
            // 对于 FnMut 的闭包,
            // 只能不可变地访问闭包外的变量
            let t = &s;
            // 闭包返回一个 u32 类型的值
            t.n * item
            });


            println!("{}", prod); // 输出 68
            }

            通常,对于只执行一次闭包的情况,选用 FnOnce(...) ;需要执行多次的情况,选用 FnMut(...)Fn(...) 要求闭包是无状态的,在要求闭包有稳定的返回值时使用。

            需要注意的是,闭包类型实际上都是一种特殊的 trait ,而非数据类型。因此需要在类型标注前加上 impl

            静态生命周期闭包

            有时,创建闭包时并不会马上调用它,而是把它存起来,在之后需要时调用。因为闭包类型实际上是 trait ,如果将闭包作为 struct 中的一个字段,常常需要用 boxed trait 的方式。例如:

              struct Product {
              name: String,
              price: u32,
              // 使用 boxed trait 的方式存放一个闭包字段
                  // 这个闭包具有 'static 生命周期
              final_price_fn: Box<dyn 'static + Fn(&Product) -> u32>,
              }

              这样的闭包具有 'static 生命周期,即闭包不能访问闭包外的非 'static 引用。(这里的 'static 也可以省略不写,因为在这种情况下它是默认的生命周期。)

              对于 'static 生命周期的闭包,闭包创建时通常需要加上 move ,表示将涉及到的变量所有权“移动到闭包环境中”。完整示例如下:

                struct Product {
                name: String,
                price: u32,
                // 使用 boxed trait 的方式存放一个闭包字段
                // 这个闭包默认了 'static 生命周期
                final_price_fn: Box<dyn Fn(&Product) -> u32>,
                }


                impl Product {
                fn new(name: String, price: u32) -> Self {
                Self {
                name,
                price,
                final_price_fn: Box::new(|this| this.price),
                }
                }


                fn set_discount(&mut self, rate: f32) {
                // 对于 'static 生命周期的闭包,
                // 如果需要访问闭包外的变量,
                // 则需要加上 move ,表示将变量移动给闭包环境
                self.final_price_fn = Box::new(move |this| {
                (this.price as f32 * rate) as u32
                });
                }


                fn final_price(&self) -> u32 {
                let f = &self.final_price_fn;
                f(self)
                }
                }


                fn main() {
                let mut product = Product::new(
                format!("Geometry"),
                60,
                );
                product.set_discount(0.75);
                let final_price = product.final_price();
                println!("{}", final_price); // 输出 45
                }

                从性能角度上看,闭包自身是零开销的;但结合 boxed trait 使用时,会具有与 boxed trait 近似的开销。

                闭包是一种重要的抽象方式、在 rust 库中很常见,具体的用例将在之后的文章中介绍。

                文章转载自最后的叶子的奇妙小屋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论