【Rust 笔记】18-宏

18 - 宏

  • assert_eq! 宏:可以生成包含断言文件名和行号的错误消息。

  • 宏:是扩展语言的一种方式。在编译期间,在检查类型和生成任何机器码之前,每个宏调用都会被扩展(expanded)。

    assert_eq!(gcd(6, 10), 2);
    
    // 上述宏扩展后的代码
    match (&gcd(6, 10), &2) {
          
          
        (left_val, right_val) => {
          
          
            if !(*left_val == *right_val) {
          
          
                panic!("assertion failed: '(left == right)', \
                    (left: '{:?}', rigth: '{:?}')", left_val, right_val);
            }
        }
    }
    
  • Rust 宏可以与语言的其他组件整合,不容易出错。

    • 宏调用始终都会以一个感叹号来标记,不容易被忽视。
    • Rust 宏永远不会插入不匹配的方括号或圆括号。
    • Rust 宏自带模式匹配,便于维护和扩展。

18.1 - 宏基础

  • macro_rules! 是 Rust 中定义宏的主要方式。

    marcro_rule! assert_eq {
          
          
        ($left: expr, $right: expr) => (  // 模式
            {
          
            // 模板
            match (&$left, &$right) {
          
          
                if !(*left_val == *right_val) {
          
          
                    panic!("assertion failed: '(left == right)' \
                        (left: '{:?}', right: '{:?}')", left_val, right_val)
                }
                }
            }
        );
    }
    
    • 注意上述代码中 assert_eq 后面没有感叹号。
    • 只有调用宏的时候才需要包含感叹号 !,定义的时候则不需要。
  • file!line!macro_rules!,本身是内置在编译器中的。

  • 宏定义定义的另一种方式:过程宏(procedural macro)。

  • macro_rule! 定义的宏完全基于模式匹配实现逻辑。

    ( 模式1 ) => ( 模板1 );
    ( 模式2 ) => ( 模板2 );
    ...
    
    • 模式和模板周围可以不用圆括号,而使用方括号或花括号。

    • 以下形式都是等价的:

      assert_eq!(gcd(6, 10), 2);
      assert_eq![gcd(6, 10), 2];
      assert_eq!{
              
              gcd(6, 10), 2}   // 在使用花括号时,最后的分号是可选的。
      
  • 括号的使用惯例或约定:

    • 在调用 assert_eq! 时,使用圆括号
    • 在调用 vec! 时使用方括号
    • 在调用 macro_rules! 时使用花括号

18.1.1 - 宏扩展基础

  • 不能在定义宏之前调用宏,Rust 对宏调用是边分析边扩展的。

  • 宏模式操作的是记号(token),比如数字、名称、标点等 Rust 程序的语法符号。注释和空白不是记号。

  • 对于宏模式中的

    $left:expr
    

    的含义:

    • expr 是表达式,其值会赋值给 $left
    • 宏模式与正则表达式一样,只有少数特殊字符会触发特殊的匹配行为。
    • 其他字符,如逗号,需要按字面匹配,否则匹配会失败。

18.1.3 - 重复

  • 标准的 vec! 宏有两种形式:

    // 重复一个值N次
    let buffer = vec![0_u8; 1000];
    
    // 一个由逗号分隔的值列表
    let numbers = vec!["udon", "ramen", "soba"];
    
  • vec! 宏的实现:

    macro_rules! vec {
          
          
      ($elem: expr; $n: expr) => {
          
          
        ::std::vec::from_elem($elem, $n)
      };
      ( $( $x:expr ),* ) => {
          
          
        <[_]>::into_vec(Box::new([ $( $x ),* ]))
      };
      ( $( $x:expr ),+ ,) => {
          
          
        vec![ $( $x ),* ]
      };
    }
    
  • 重复的模式:

    模式 含义
    $( ... )* 匹配 0 或多次,没有分隔符
    $( ... ),* 匹配 0 或多次,以逗号分隔
    $( ... );* 匹配 0 或多次,以分号分隔
    $( ... )+ 匹配 1 或多次,没有分隔符
    $( ... ),+ 匹配 1 或多次,以逗号分隔
    $( ... );+ 匹配 1 或多次,以分号分隔
    • $x 是一组表达式;
    • <[_]> 表示某种类型值的切片。
    • , 表示匹配带有额外逗号的列表。

18.2 - 内置宏

  • file!():扩展为一个字符串字面量,即当前文件名。

  • line!():扩展为一个 u32 字面量,代表当前行(从 1 开始)

  • column!():扩展为一个 u32 字面量,代表当前列(从 0 开始)

  • stringify!(...tokens...):扩展为一个字符串字面量,包含给定的记号。

    • assert! 使用此内置宏生成包含断言代码的错误消息。
    • 如果参数包含一个宏,那么不会发生扩展,如 stringify(line!()),任意参数为一个字符串 "line!()"
  • concat!(str0, str1, ...):扩展为一个字符串字面量,是拼接其参数之后的结果。

  • cfg!(...):扩展为一个布尔值常量,如果当前构建配置与括号中的条件匹配,则为 true

  • env!("VAR_NAME"):扩展为一个字符串,即指定环境变量在编译时的值。如果指定的变量不存在,会发生编译错误。常与 Cargo 结合使用。

  • option_env!("VAR_NAME"):与 env! 相同,不过返回 Option<&'static str>,如果指定变量没有设置,则返回 None

  • include!("file.rs"):扩展为指定文件的内容,必须是有效的 Rust 代码,比如表达式或特性项的序列。

  • include_str!("file.txt"):扩展为一个 &'static str,包含指定文件的文本。

    • 使用方法:

      const COMPOSITOR_SHADER: &str =
          include_str!("../resources/compositor.glsl");
      
    • 如果指定的文件不存在,或者文本不是有效 UTF-8,会导致编译错误。

  • include_bytes!("file.dat"):将文件作为二进制数据进行扩展。其结果是 &'static [u8] 类型的值。

  • 内置宏的规则:

    • 在编译时被处理,如果文件不存在或不能读,那编译就会失败。
    • 所有宏不能在运行时失败。
    • 在任何情况下,如果文件名是相对路径,就会相对于包含当前文件的目录来解析。

18.3 - 调试宏

  • 宏扩展的过程是不可见的。

    • Rust 在扩展宏的过程中,在发现某些错误时会打印出一条错误消息。
    • 但不会显示包含错误的完全扩展后的代码。
  • rustc
    

    可以展示代码在扩展所有宏之后的信息。

    • cargo build --verbose 可以看到 Cargo 时如何调用 rustc
    • 即复制 rustc 命令,再添加 -Z unstable-options --pretty expanded 选项。
  • log_syntax!()
    

    宏:在编译时,可以把其参数打印到终端。

    • 可以用于实现类似 println! 的调试。
    • 要求带有#![feature(log_syntax)] 特性标志。
  • 让 Rust 编译器把所有宏调用的日志打印到终端上。

    • 在代码某个地方插入 trace_macros!(true);
    • 这样,Rust 每扩展一个宏,都会打印宏的名字和参数。

18.4 - 自定义一个宏 ——json!

开发一个 json! 宏,接收一个 JSON 值作为参数,然后为其扩展为类似下述 Rust 表达式:

let students = json!([
  {
    
    
    "name": "Jim Blandy"
    "class_of": 1926
    "major": "Tibetan throat singing"
  },
]);

18.4.1 - 片段类型

  • macro_rules! 宏支持的片段类型:

    片段类型 匹配(示例) 后面可以加…
    expr 表达式:2 + 2, "udon", x.len() => , ;
    stmt 表达式或声明,不包含末尾的分号(优先使用 exprblock => , ;
    ty 类型:StringVec<u8>(&str, bool) => , ; =
    path 路径:ferns::std::sync::mpsc => , ; =
    pat 模式:_Some(ref x) => , =
    item 特性项:struct Point {x: f64, y: f64}mod ferns; 不限
    block 代码块:s += "ok\n"; true 不限
    meta 属性体:inlinederive(Copy, Clone)doc="3D models." 不限
    ident 标识符:stdJsonlongish_variable_name 不限
    tt 记号树:;>={}[0 1 (+ 0 1)] 不限
  • json! 宏的定义如下:

    macro_rules! json {
          
          
      (null) => {
          
          
        Json::Null
      };
      ([ $( $element:tt ),* ]) => {
          
          
        Json::Array(...)
      };
      ({
          
           $( $key:tt : $value:tt ),* }) => {
          
          
        Json::Object(...)
      };
      ($other:tt) => {
          
          
        ... // TODO: 返回Number、String或Boolean
      };
    }
    

18.6 - 超越 macro_rules!

  • 过程宏:
    • 支持扩展#[derive] 属性,以处理自定义特型。
    • 作为 Rust 函数实现,并非一个声明性的规则集。

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

猜你喜欢

转载自blog.csdn.net/feiyanaffection/article/details/125575445