宏与函数的区别

宏是一种为写其它代码而写代码的方式,即元编程derive属性来生成各种trait的实现,println!宏和vec!宏展开来生成比手写代码更多的代码。

  • 函数签名必须声明函数参数个数和类型。宏能够接受不同数量的参数
  • 宏可以在编译器翻译代码前展开,使用宏可以在一个给定类型上实现trait。而函数不行,因为函数运行时才调用,而trait是在编译期实现的。
  • 宏定义比函数定义更难阅读、理解和维护。

在一个文件里调用宏之前必须定义它或者引入作用域。而函数可以在任何地方定义和调用。

rust中由使用macro_rules!声明宏,和三种过程宏

  • 自定义#[derive]宏在结构体上和枚举上指定通过derive属性添加的代码。
  • 类属性宏定义可用于任意项的自定义属性。
  • 类函数宏看起来像函数但是作用于作为参数传递的token。

使用macro_rules!的声明宏

声明宏是Rust最常用的宏形式。编写类似match表达式的代码,将一个值和包含相关代码的模式进行比较。不过值是传递给宏的Rust源代码的字面值,模式用于和前面提到的源代码字面值进行比较,每个模式的相关代码会替换传递给宏的代码。

例如利用vec!宏来创建给定初始元素的vector:

let v: Vec<u32> = vec![1, 2, 3];

vec!宏的简易后的定义如下:

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

这里就依次为了好好说明声明宏的结构。

首先是#[macro_export]注解,这个注解表示,主要导入了定义了这个宏的crate,那么就可以使用该宏。没有该注解,这个宏不能被引入作用域

然后是macro_rules!和宏名称开始宏定义,且所定义的宏并不带感叹号。后面的花括号内是宏定义体。

在宏定义体中有一个分支模式($($x: expr), *),这个模式匹配的话,就执行=>对应的模式相关的代码块。这里的模式语法与前面提到的模式语法是不同的,宏模式匹配的是Rust代码结构而不是值,宏模式语法参考宏模式语法。这里大致介绍一下宏模式语法:

  • 首先,一对括号包含了整个模式
  • 接下来是$()捕获括号内模式的值用以在替代代码中使用。这里的模式是$x:expr匹配Rust的任意表达式,并将该表达式命名为$x
  • $()后面的是一个可有可无的逗号分隔符,紧随着逗号的*表示该模式匹配零个或者多个*之前的模式。
  • 匹配的结果会在=>后面的关联代码块的$()*中生成零次或者多次内部的语句,用匹配的值替换。

vec![1,2,3]调用这个宏的时候,$x模式分别于三个表达式1,2,3进行了三次匹配。然后生成了三次$()*中的temp_vec.push($x);语句,其中$x会被匹配的值替换。于是该宏最后等价于下面的语句:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

在将来,会采用macro关键字来替换macro_rules!,因为它存在一些极端情况。

从属性生成代码的过程宏

过程宏比起声明宏更像函数。接受Rust代码作为输入,在这些代码上进行操作,然后产生另外一些代码作为输出。而不是像声明宏一样匹配模式然后以另一部分代码替换当前代码。

创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。使用这些宏需要采用如下形式:

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

定义过程宏的函数接受一个TokenStream作为输入并生成TokenStream作为输出。TokenStream是定义在proc_macro crate中代表一系列token的类型,Rust默认携带了proc_macro crate。

宏所处理的源代码组成了TokenStream, 宏生成的代码是输出TokenStream。函数上属性指明了创建的过程宏的类型。同一crate中可以由多种过程宏。

编写自定义derive宏

创建一个hello_macro crate,包含名为HelloMacro的trait和关联函数hello_macro。并提供一个过程宏让用户可以使用#[derive[HelloMacro]来注解类型得到hello_macro函数的默认实现。与#[derive(Debug)]类似。

hello_macro的默认实现为打印Hello, Macro! My name is Typename,其中的Typename为实现了trait的具体的类型名。

其使用方式如下:

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
 
#[derive(HelloMacro)]
struct Pancakes;
 
fn main() {
    Pancakes::hello_macro();
}
 

创建一个crate

$ cargo new hello_macro --lib

在/src/lib.rs下编写如下代码:

pub trait HelloMacro {
    fn hello_macro();
}

在hello_macro下创建一个crate实现过程宏。

$ cargo new hello_macro_derive --lib

在hello_macro_derive/Cargo.toml添加依赖:

[lib]
proc-macro = true
 
[dependencies]
syn = "1.0" // 用来将Rust代码变为用户能够处理的AST树结构
quote = "1.0" // 用来将AST树结构变为Rust代码
 

在hello_macro_derive/src/lib.rs中实现如下代码:

extern crate proc_macro;
 
use proc_macro::TokenStream;
use quote::quote;
use syn;
 
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();
 
    // Build the trait implementation
    impl_hello_macro(&ast)
}
 
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    // quote!宏支持模板语法。#name用name变量的值替换。stringfy!将值用双引号包裹。
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    gen.into()
}

创建一个二进制项目,依赖这两个crate,在src/main.rs编写如下代码:

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
 
#[derive(HelloMacro)]
struct Pancakes;
 
fn main() {
    Pancakes::hello_macro();
}
 

通过#[derive[HelloMacro]注解就可以为Pancakes生成实现HelloMacro trait关联函数的代码。

类属性宏

与自定义派生宏类似,不同的是,derive属性生成代码,类属性宏能够创建新的属性。derive只能用于结构体和枚举,但是类属性还可以用于函数等

下面以一个实例进行说明。创建一个名为router的属性用于注解web应用程序框架的函数

#[route(GET, "/")]
fn index() {

#[route] 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

两个 TokenStream 类型的参数;第一个用于属性内容本身,也就是 GET, "/" 部分。第二个是属性所标记的项:在本例中,是 fn index() {} 和剩下的函数体。

除此之外,类属性宏与自定义派生宏工作方式一致:创建 proc-macro crate 类型的 crate 并实现希望生成代码的函数!

类函数宏

类函数(Function-like)宏的定义看起来像函数调用的宏。比函数更灵活;例如,可以接受未知数量的参数。这样说与macro_rules!有些类似,但是macro_rules!只是简单的替换,而类函数宏能够进行更加复杂的逻辑判断。

一个类函数宏例子是可以像这样被调用的 sql! 宏:

let sql = sql!(SELECT * FROM posts WHERE id=1);

这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这就是macro_rules!宏无法做到的。

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

与自定义派生宏类似。