十六、宏
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);