宏与函数的区别
宏是一种为写其它代码而写代码的方式,即元编程。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 {与自定义派生宏类似。