十八、Rust 进阶特性 (二)

294 阅读5分钟

十八、Rust 进阶特性 (二)

1. 高级类型

1.1 类型安全和抽象

  • 通过使用 newtype 模式来实现类型安全和抽象

  • 使用 newtype 模式的优点:

    • 可以抽象掉一些类型的实现细节,只暴露出期望的公有 API,以限制其功能
    • 可以隐藏内部的泛型类型,只提供特定的类型

1.2 类型别名

  • 类似于 Typescript 里的类型别名

    type EpochTimeStamp = number;
    
    • 关键字:type

    • 示例:type Kilometers = i32;

    • 一个重要作用:减少重复

      // 来自官方教程的一个示例
      type Thunk = Box<dyn Fn() + Send + 'static>;
      
      let f: Thunk = Box::new(|| println!("hi"));
      
      fn takes_long_type(f: Thunk) {
          // --snip--
      }
      
      fn returns_long_type() -> Thunk {
          // --snip--
          Box::new(|| ())
      }
      
      

1.3 特殊类型 !

  • empty type
  • 作用:在函数从不返回的时候充当返回值
    • 类似于 Typescript 里的 never
  • 示例:fn bar() -> ! {}
  • 从不返回的函数被称为 发散函数 (diverging functions)
  • match 语句的 continue 返回的就是这个类型的值,即 !

1.4 动态大小类型

  • 动态大小类型:dynamic sized type, DST

  • Rust 需要知道应该为某一类型的值分配多少内存,且同一类型的值必须使用相同大小的内存

  • 不可能创建一个存放动态大小类型的变量:不同的值,其大小不相同,这违背了上述规则

  • 常规用法:通过引入额外的 meta info 来存储动态信息的大小,如 &str 类型

  • Sized trait:决定一个类型的大小是否在编译时可知

    • Rust 隐式地为每一个泛型函数增加了 Sized bound
  • 放宽泛型的 Sized 限制

    • fn generic<T: ?Sized>(t: &T) {}:将 Sized 变成可选的

2. 高级函数与闭包

2.1 函数指针

  • 类似于 Javascript 里的回调函数,直接传递一个函数引用进入

  • 函数指针实现了三个闭包 trait(FnFnMutFnOnce) ,因此期望闭包作为参数的函数,也可以在对应参数位置传入一个函数指针

    • 注意:反之则不成立,在只接受函数指针作为参数的地方,有可能不接受传入闭包作为参数。比如与不存在闭包的外部代码交互时
      • C 语言的函数可以接受函数作为参数,但 C 语言没有闭包
  • 使用示例

    fn add_one(x: i32) -> i32 {
        x + 1
    }
    
    // 注意这个函数签名
    fn do_twice<T = i32>(f: fn(T) -> T, arg: T) -> T {
        f(arg) + f(arg)
    }
    
    fn main() {
        let answer = do_twice(add_one, 5);
    
        println!("The answer is: {}", answer);
    }
    
    

2.2 闭包作为返回值

  • 闭包表现为 trait,这意味着不能 直接 返回闭包

  • trait 作为返回值

    • 使用实现了期望返回的 trait 的具体类型来作为函数的返回值
  • 闭包作为返回值

    • 使用 Box<T> 来包裹期望返回的闭包

      // 来自官方教程的一个示例
      
      // 注意这里的返回值类型定义,是 Box<dyn Fn>
      fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
          // 注意这里的返回值,是被 Box 包裹的
          Box::new(|x| x + 1)
      }
      
      

3. 宏

  • 作用:提供元编程能力,提高封装度,减少代码量

  • 宏与函数的区别

    • 宏提供了函数的能力,且提供了一些额外的能力:
      • 宏可以在编译器翻译代码前展开
      • 宏可以在一个给定类型上实现 trait
    • 宏的缺点:宏定义比函数定义更复杂、更难阅读、更难理解、更难维护
    • 在调用宏 之前 就必须定义并引入作用域,而函数则可以在任何地方定义和调用
  • Rust 中,宏 (macros) 分为两种:

    • 使用 macro_rules! 声明的自定义宏

      • 允许 developer 编写一些类似 match 表达式的代码

      • 一个示例

        #[macro_export] // 如果没有这个注解,那么这个宏就不能被引入作用域
        
        // 宏的名称后面是没有 `!` 的,要注意哦
        macro_rules! vec { // 大括号里面的就是宏定义体
            // ↓ 下面这个是一个单边模式。一个宏可以有大于零的任意个单边模式
            //   单边模式匹配的是 Rust 代码结构,而不是值
            ( $( $x:expr ),* ) => {
                {
                    let mut temp_vec = Vec::new();
                    $(
                        temp_vec.push($x);
                    )*
                    temp_vec
                }
            };
        }
        
        
    • 过程 (Procedural) 宏

      • 作用:从属性生成代码

      • 原理:接收 Rust 代码作为输入,然后在这些代码上进行操作,最终产生新的代码作为输出

      • 特点:

        • 过程宏包含一个函数,这也是其得名的原因
        • 这个函数接受一个 TokenStream 作为输入,最终产生一个 TokenStream 作为输出
          • 宏需要处理的代码即为输入 TokenStream
          • 宏生成的代码即为输出 TokenStream
        • 这个函数上有一个属性,这个属性表明了这个过程宏的类型
          • 分类:

            • 自定义 #[derive] 宏:在结构体和枚举上,通过指定 derive 来添加的代码

              • derive 只能用于结构体和枚举

              • 截至目前 (v1.60.0),过程式宏必须在其自己的 crate 内,该限制最终可能被取消

              • 示例代码

                // 来自官方教程的一个示例
                
                // src/main.rs
                use hello_macro::HelloMacro;
                use hello_macro_derive::HelloMacro;
                
                // 在这个结构体上使用自定义的宏
                #[derive(HelloMacro)]
                struct Pancakes;
                
                fn main() {
                    // 所以这里就可以使用宏里定义的函数了
                    Pancakes::hello_macro();
                }
                
                
            • 类属性宏:定义了壳用于任意元素的自定义属性

              • 与自定义派生宏相似,不同于为 derive 属性生成代码,它允许你创建新的属性

              • 除了可用于枚举和结构体外,还能用于其他项,比如函数

              • 实现方式:与自定义派生宏工作方式一致。先创建 proc-macro crate 类型的 crate,再实现希望生成代码的函数

              • 一个使用示例

                // 使用类属性宏
                #[route(GET, "/")]
                fn index() {}
                
                // 定义类属性宏
                #[proc_macro_attribute]
                pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}
                
                
            • 类函数宏:看起来像函数,但作用于作为参数传递的 token

              • 看起来像函数调用的宏,但比函数更灵活

                • 比如:可以接受未知数的参数
              • 一个使用示例

                // 使用类函数宏
                let sql = sql!(SELECT * FROM posts WHERE id=1);
                
                // 定义类函数宏
                #[proc_macro]
                pub fn sql(input: TokenStream) -> TokenStream {}