宏
你有没有注意到rust中的函数参数都是固定数量的,而print!
宏和vec!
宏的参数数量却是任意的?这一节我们就看一下宏的用法。宏(macro)是rust中的是某一组相关功能的集合名字,它包含声明宏(declarative macro)
和过程宏(procedural macro)
,别急,我们先来看一些概念性的东西,然后分别上手尝试下
声明宏是什么:
声明宏
使用macro_rules!
宏来定义,比如println!
和vec!
都可以使用macro_rules!
宏来定义
过程宏是什么:
- 还记得我们为结构体和枚举使用属性
#[derive(Debug)]
来为结构体实现Debug trait
吗?这就是过程宏
的能力了,这种称之为自定义derive宏
- 除了为结构体和枚举添加
自定义derive宏
之外,过程宏
还有一种能力,可以任意的条目(比如函数)添加自定义属性的属性宏
- 还有一种
函数宏
,它把编译器产出的词法token
作为参数,然后构建出新代码
宏与函数有什么差别吗?
- 宏是一种用于编写其他代码的代码编写方式,也就是所谓的元编程范式(metaprogramming)
- 函数在定义签名时必须声明自己参数的个数与类型,而宏则能够处理可变数量的参数
- 宏的定义要比函数定义复杂得多,宏定义通常要比函数定义更加难以阅读、理解及维护
- 当在某个文件中调用宏时,必须在调用前定义宏或将宏引入当前作用域中,而函数则可以在任意位置定义并在任意位置使用
声明宏macro_rules!
声明宏
也被称作模板宏,它会将输入的值与带有相关执行代码的模式进行比较:此处的值是传递给宏的字面rust源代码,而此处的模式则是可以用来匹配这些源代码的结构。在编译时,当某个模式匹配成功时,该分支下的代码就会被用来替换传入宏的代码,巴拉巴拉一大堆书中的定义,下面我们先回一下vec!
宏的使用:
let v: Vec<_> = vec![1, 2, 3];
println!("{:?}", v);
// [1, 2, 3]
我们来尝试实现一个简化版vec!
的定义:
macro_rules! vec2 {
// $()中表示模式,想想一下正则中的分组功能。
// 其中expr表示匹配表达式,$item是将匹配的表达式进行命名。
// 括号后面的逗号意味着可能有多个参数,
// *号表示前面的所有字符(包括$()和,)出现0到多次(这里和一般的正则不同哦)
( $( $item:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($item);
)* // 匹配了多少次参数,就编译出多少次这个语句
temp_vec
}
}
}
在编译阶段宏会被展开成下面的样子:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
来使用一下:
let v2 = vec2![1,2,3];
println!("{:?}", v2);
// [1, 2, 3]
声明宏
是根据模式匹配来替换代码的行为,而过程宏
主要是操作输入的rust代码,并生成另外一些rust代码作为结果。当前由于技术原因,当创建过程宏
时,宏的定义必须单独放在它们自己的包中,并标注的包类型。
过程宏之自定义derive宏
仔细阅读这段话哦,这次我们创建一个hello_macro
的包,并在其中定义一个拥有关联函数hello_macro
的HelloMacro trait
。提供一个能够"自动实现trait的过程宏
"。然后呢,开发者只需要在他们类型上标注#[derive(HelloMacro)]
,就可以得到hello_macro
函数的默认实现,调用hello_macro
函数后,会打印文本:"Hello Macro! MyName is TypeName",其中的TypeName
替换为开发者的类型的名称,就像下面这样:
// hello_macro/src/main.rs
use hello_macro::HelloMacro; // 引入trait
use hello_macro_derive::HelloMacro; // 引入自定义derive宏
#[derive(HelloMacro)] // 使用自定义derive宏实现HelloMacro trait
struct Pancakes;
Pancakes::hello_macro();
// Hello Macro! MyName is Pancakes!
首先我们定义 HelloMacro trait 以及其关联函数:
// hello_macro/src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
然后定义过程宏
,过程宏
需要被单独放置到它们自己的包内(未来可能会取消这个限制),实现一个自定义派生过程宏
的包,命名习惯一般是:包名称_derive
,在hello_macro
项目同一级别文件夹中创建一个名为hello_macro_derive
的包:
cargo new hello_macro_derive --lib
当前目录结构如下:
├── hello_macro
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── main.rs
└── hello_macro_derive
├── Cargo.toml
└── src
└── lib.rs
实现一个过程宏
还需要三个陌生的包,我们只需要简单了解它们的用处就好:
proc_macro
:这个包是rust内置,编译器用来读取和操作rust代码的APIsyn
:用来解析rust代码产生抽象语法树ast
quote
:将syn
产生的语法树重新生成rust代码
然后将这三个包添加到依赖中(是不是很像前端的babel工具系列),注意需要使用proc-macro = true
来声明这是一个这个包是过程宏(proc-macro)
的包:
// hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
准备了半天,重头戏来了,开始编写过程宏
:
// hello_macro_derive/src/lib.rs
use proc_macro::TokenStream; // 词法token类型
use quote::quote;
use syn;
// 当开发者在一个类型上指定#[derive(HelloMacro)]时,
// hello_macro_derive函数将会被调用
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 将rust语法转为抽象语法树
let ast = syn::parse(input).unwrap();
// 实现hello_macro
impl_hello_macro(&ast)
}
解析之后的ast
结构如下:
DeriveInput {
// --略--
ident: Ident {
ident: "Pancakes", // 我们需要获取这个字段
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
继续实现上面的impl_hello_macro
函数:
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
// 获取"Pancakes"
let name = &ast.ident;
// quote! 宏可以用来编写rust代码,这里生成一段为Pancakes实现HelloMacro trait的代码
let gen = quote! {
// 这里使用了模板机制,#name用来引用"Pancakes"
impl HelloMacro for #name {
fn hello_macro() {
// 由于要打印出类型的名称,所以这里也需要用#name来替换为"Pancakes"
// 这里使用的stringify! 宏是内置在rust中的,它接收一个rust表达式,在编译时将这个表达式转换成字符串字面量,如下:
// println!("{}", stringify!(1 + 2));
// 1 + 2
println!("Hello Macro! MyName is {}", stringify!(#name))
// 代码中输入的#name有可能是一个表达式,因为我们希望直接打印出这个值的字面量,所以这里使用了stringify!
}
}
};
// quote!宏执行的直接结果并不是编译器所期望的并需要转换为TokenStream。
// 为此需要调用into方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 TokenStream 类型值。
gen.into()
}
最后还需要在hello_macro
包中引用hello_macro_derive
:
// hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { path="../hello_macro_derive" }
完成之后在hello_macro
项目中运行cargo run
,便可以看到控制台输出:Hello Macro! MyName is Pancakes
过程宏之属性宏
与自定义derive宏
类似,属性宏
允许创建新的属性,而不是为derive属性生成代码,自定义derive宏
只能被用于结构体和枚举,而属性则可以同时被用于其他条目,比如函数等,比如我们编写Web应用框架接口时为函数添加接口方法和路径:
#[route(GET, "/")]
fn index() {}
使用#[proc_macro_attribute]
属性将一个函数定义为一个过程宏
。其宏定义的函数签名看起来像这样:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}
拿上边编写Web应用框架接口的例子来说,route
参数中,attr
指代属性部分:#[route(GET, "/")]
,item
指代函数体:fn index() {}
,总体来说还是和自定义derive宏
使用方式很相似的,介于篇幅原因就不多介绍了,大家可以先把基础知识打扎实,有精力的话再去深入研究
过程宏之函数宏
函数宏
可以定义出类似于函数调用的宏,与macro_rules!
宏类似,函数宏
也能接收未知数量的参数
函数宏与macro_rules的区别是什么?
macro_rules!
宏只能使用类似于match
的语法来进行定义,而函数宏
则可以接收一个TokenStream
作为参数,与自定义derive宏
和属性宏
一样,可以在定义中使用rust代码来操作TokenStream
,例如下面这个sql!
宏:
let sql = sql!(SELECT * FROM posts WHERE id=1);
sql!
宏的签名是这样的:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {}
函数宏
与自定义derive宏
的签名类似,接收TokenStream
作为参数,然后返回生成的代码,所以你发现了,声明宏
就是用类似正则匹配的方式,而三种过程宏
都是对rust代码的编辑再生成,所以声明宏
肯定是没有过程宏
灵活的,过程宏
也没有声明宏
编写起来高效,本篇只是简单介绍了宏的能力,如果想更加深入,可以去阅读宏小册
最后
本篇是rust入门系列的最后一篇,希望大家对rust有了基本的了解,后面可能会有一些小demo的文章供大家练手,本人也在学习过成功,所以希望可以和大家多多交流,共同进步吧^_^。
封面图:by 铁柱呆又呆
关注「码生笔谈」公众号,阅读更多有趣文章