Wang's blog

十六、宏

Published on

分类

Rust中的宏分为两类:使用macro_rules!声明的声明式宏与以下3种过程宏:

  • 自定义#[derive]宏:可以使用derive属性在结构体与枚举上添加指定代码
  • 类属性宏:自定义可以在任何项目上使用的属性
  • 类函数宏:看起来像函数但是在它们的参数上进行操作

为什么使用宏

  • DRY(不要重复自己):可以避免重复同样的代码
  • DSL(领域特定语言):可以定义用于特定用途的自定义语法
  • Variadic(可变参数函数):可以定义接收任意数量参数的接口(类似函数),例如println!

宏与函数的不同

宏是用代码写其它代码的方式,即元编程。在编译前,编译器会将宏展开并进行编译。与C等语言的宏不同,Rust的宏不是简单的字符串展开,而是会生成抽象语法树,这避免了无法预期的问题。宏与函数都是用于减少代码量,但是宏有一些函数没有的能力。

  • 函数必须指定参数的个数与类型,宏可以接收任意数量参数
  • 宏在编译前展开,函数在运行期间调用,所以宏可以做许多函数无法做的事情,例如在一个类型上实现一个特性
  • 宏的缺点是更加复杂,难于理解与维护,因为是在间接地用代码编写代码
  • 宏必须先定义并加引入作用域才能使用,而函数可以在任何地方定义与调用

使用macro_rules!进行元编程的声明式宏

声明式宏允许编写一个类似match的结构,在编译时,如果传入值匹配其中一个分支,则将宏替换为对应的代码。使用macro_rules!定义宏:

// 定义宏
macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

// 调用宏
say_hello!()

语法

参数与指示符

宏的参数定义方式如下,每个参数带有前缀$,冒号后为其指示符,表示该参数的类型。

($func_name:ident)

可能的指示符有:

  • block:块
  • expr:表达式
  • ident:标识符,如变量、函数名
  • item:项,如函数、结构体、模块等
  • literal:字面值常量
  • pat:模式
  • path:路径
  • stmt:语句
  • tt:语法树
  • ty:类型
  • vis:visibility限定词

完整的列表在这里

重载

宏可以重载,使用不同的参数组合。

macro_rules! test {
    // 一个参数组合
    ($left:expr; and $right:expr) => {
        println!("{:?} and {:?} is {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left && $right)
    };

    // 另一个参数组合
    ($left:expr; or $right:expr) => {
        println!("{:?} or {:?} is {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left || $right)
    };
}

可变参数列表

使用+表示参数数量可以为一个或多个;使用*表示参数可以为零个或多个。

macro_rules! find_min {
    // 只接收一个参数
    ($x:expr) => ($x);
    // 接收两个及以上参数
    ($x:expr, $($y:expr),+) => (
        std::cmp::min($x, find_min!($($y),+))
    )
}

从属性生成代码的过程宏

过程宏更像函数,它们接受一些代码作为输入,进行操作后产生一些代码作为输出。例如:

use proc_macro;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

此函数定义了一个过程宏,它接受一个TokenStream作为输入,并产生一个TokenStream作为输出。此外它还有一个属性表示它是哪种宏。

编写自定义derive宏

假设有一个特性和一个结构体:

pub trait HelloMacro {
    fn hello_macro();
}

struct Pancakes;

用户可以自行为Pancakes结构体实现HelloMacro特性:

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

但是,对于每个想要使用HelloMacro特性的结构体,用户都需要手动实现。我们希望可以使用宏省去这项工作。

// 说明是一个derive过程宏,为HelloMacro特性生成代码
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 将输入的TokenStream解析为可理解与操作的DeriveInput结构
    let ast = syn::parse(input).unwrap();

    // 实现宏的功能
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    // 生成代码
    let gen = quote! {
        // 实现所需功能
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    // 将输出转换为TokenStream
    gen.into()
}

之后便可以使用[derive(HelloMacro)]自动生成代码。

// 使用该宏自动生成为Pancakes类型实现HelloMacro特性的代码
#[derive(HelloMacro)]
struct Pancakes;

类属性宏

类属性宏与自定义derive宏类似,但不是为derive属性生成代码,而是创建新的属性。derive属性只能在结构体或枚举上使用,类属性宏更加灵活,可以使用在其它项目上,比如函数。

// 在函数上使用route属性
#[route(GET, "/")]
fn index() {}

// 实现route属性,这里有2个TokenStream参数,第1个对应属性(GET, "/"部分),第2个是属性作用的项目(fn index() {})
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}

类函数宏

类函数宏定义类似函数的宏,与macro_rules!定义的宏类似,此类宏比函数更加灵活,比如,它们可以接受未知数量的参数。但是macro_rules!只能使用类似match的语法,而类函数宏使用TokenStream作为输入,并使用Rust代码对其进行操作,返回想要生成的代码。

// 定义sql宏
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {}

// 使用sql宏
let sql = sql!(SELECT * FROM posts WHERE id=1);