# 宏和函数的区别
宏和函数的区别并不少,而且对于宏擅长的领域,函数其实是有些无能为力的。
# 元编程
从根本上来说,宏是通过一种代码来生成另一种代码,如果大家熟悉元编程,就会发现两者的共同点。
derive
属性会自动为结构体派生出相应特征所需的代码,例如 #[derive(Debug)]
,还有熟悉的 println!
和 vec!
,所有的这些宏都会展开成相应的代码,且很可能是长得多的代码。
总之,元编程可以帮我们减少所需编写的代码,也可以一定程度上减少维护的成本,虽然函数复用也有类似的作用,但是宏依然拥有自己独特的优势。
# 可变参数
Rust 的函数签名是固定的:定义了两个参数,就必须传入两个参数,多一个少一个都不行,对于从 JS/TS 过来的同学,这一点其实是有些恼人的。而宏就可以拥有可变数量的参数,例如可以调用一个参数的 println!("hello")
,也可以调用两个参数的 println!("hello {}", name)
。
# 宏展开
由于宏会被展开成其它代码,且这个展开过程是发生在编译器对代码进行解释之前。因此,宏可以为指定的类型实现某个特征:先将宏展开成实现特征的代码后,再被编译。而函数就做不到这一点,因为它直到运行时才能被调用,而特征需要在编译期被实现。
# 宏的缺点
相对函数来说,由于宏是基于代码再展开成代码,因此实现相比函数来说会更加复杂,再加上宏的语法更为复杂,最终导致定义宏的代码相当地难读,也难以理解和维护。
# 宏的分类
Rust 中的宏相较 C/C 更为强大。C/C 中的宏在预处理阶段可以展开为文本,Rust 的宏则是对语法的扩展,是在构建语法树时,才展开的宏。
Rust 中宏可以分为很多类,包括通过 macro_rules 定义的声明式宏和三种过程式宏。
- 声明式宏(Declarative macros)使得你能够写出类似 match 表达式的东西,来操作你所提供的 Rust 代码。它使用你提供的代码来生成用于替换宏调用的代码。
- 过程宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)
TokenStream
到另一个TokenStream
的函数,用输出的结果来替换宏调用。
有三种类型的过程宏:
- 派生宏(Derive macros):适用于结构、枚举和联合,并使用
#[derive(MyMacro)]
声明进行注释。它们还可以声明辅助属性,这些属性可以附加到项目的成员(例如枚举变体或结构字段)。 - 类属性式宏(Attribute-like macros):类属性式宏能够让你创建一个自定义的属性,该属性将其自身关联一个项(item),并允许对该项进行操作。它也可以接收参数。类似于派生宏,但可以附加到更多项,例如特征定义和函数。
- 类函数式宏(Function-like macros):类函数宏类似于声明式宏,因为它们是用宏调用运算符调用的
!
,看起来像函数调用。它们对您放在括号内的代码进行操作。
# 声明宏的用法
在 Rust 中,应用最广泛的一种宏就是声明式宏,类似于模式匹配的写法,将传入的 Rust 代码与预先指定的模式进行比较,在不同模式下生成不同的代码。
使用 macro_rules!
来定义一个声明式宏。
最基础的例子是很常见的 vec!
:
let v: Vec<u32> = vec![1, 2, 3]; |
简化版的定义是(实际的版本有其他分支,而且该分支下要预先分配内存防止在 push 时候再动态分划)
#[macro_export] | |
macro_rules! vec { | |
( $( $x:expr ),* ) => { | |
{ | |
let mut temp_vec = Vec::new(); | |
$( | |
temp_vec.push($x); | |
)* | |
temp_vec | |
} | |
}; | |
} |
::: $( $x:expr ),*
和 $( $x:expr,)*
的区别是什么?
前者,最后的 ,
是 MacroRepSep,意味着 1,2,3
是一个合法的序列。
后者,最后的 ,
是 MacroMatch 的一部分,意味着 1,2,3,
才是一个合法的序列。
#[macro_export]
标签是用来声明:只要 use 了这个 crate,就可以使用该宏。同时包含被 export 出的宏的模块,在声明时必须放在前面,否则靠前的模块里找不到这些宏。
按照官方文档的说法, macro_rules!
目前有一些设计上的问题,日后将推出新的机制来取代他。但是他依然是一个很有效的语法扩展方法。
这里一个注意点是:如果想要创建临时变量,那么必须要像上面这个例子这样,放在某个块级作用域内,以便自动清理掉,否则会认为是不安全的行为。
# 声明宏的限制
声明式宏有一些限制,有些是与 Rust 宏本身有关,有些则是声明式宏所特有的:
- 缺少对宏的自动完成和展开的支持
- 声明式宏调式困难
- 修改能力有限
- 更大的二进制
- 更长的编译时间(这一条对于声明式宏和过程宏都存在)
# 过程宏的使用
- cargo new custom 新建一个名为 custom 的工程。
- cd custom && cargo new custom-derive 在 custom 内新建一个名为 custom-derive 用于编写过程宏。
custom Cargo.toml
[package] | |
name = "custom" | |
version = "0.1.0" | |
[dependencies] | |
custom-derive={path="custom-derive"} |
custom-derive Cargo.toml
[package] | |
name="custom-derive" | |
version="0.1.0" | |
[lib] | |
proc-macro = true # 使用过程宏 | |
[dependencies] | |
# quote = "1.0.9" # 目前没用到,先注释了 | |
# proc-macro2 = "1.0.27" | |
# syn = {version="1.0.72", features=["full"]} |
项目结构:
.
├── Cargo.toml
├── custom-derive
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── src
│ └── main.rs
- lib.rs
use proc_macro::TokenStream; | |
extern crate proc_macro; | |
// 函数式宏 | |
#[proc_macro] | |
pub fn make_hello(item: TokenStream) -> TokenStream { | |
let name = item.to_string(); | |
let hell = "Hello ".to_string() + name.as_ref(); | |
let fn_name = | |
"fn hello_".to_string() + name.as_ref() + "(){ println!(\"" + hell.as_ref() + "\"); }"; | |
fn_name.parse().unwrap() | |
} | |
// 属性宏 (两个参数) | |
#[proc_macro_attribute] | |
pub fn log_attr(attr:TokenStream, item:TokenStream)->TokenStream{ | |
println!("Attr:{}", attr.to_string()); | |
println!("Item:{}", item.to_string()); | |
item | |
} | |
// 派生宏 | |
#[proc_macro_derive(Hello)] | |
pub fn hello_derive(input: TokenStream)-> TokenStream { | |
println!("{:?}", input); | |
TokenStream::new() | |
// 如果直接返回 input,编译会报重复定义,说明派生宏用于扩展定义 | |
// input | |
} |
TokenStream
相当编译过程中的语法树的流。
- main.rs
extern crate custom_derive; | |
use custom_derive::log_attr; | |
use custom_derive::make_hello; | |
use custom_derive::Hello; | |
make_hello!(world); | |
make_hello!(张三); | |
#[log_attr(struct, "world")] | |
struct Hello{ | |
pub name: String, | |
} | |
#[log_attr(func, "test")] | |
fn invoked(){} | |
#[derive(Hello)] | |
struct World; | |
fn main() { | |
// 使用 make_hello 生成 | |
hello_world(); | |
hello_张三(); | |
} |
make_hello 使用 #[proc_macro]
,定义自动生成一个传入参数函数。
Hello world | |
Hello 张三 |
log_attr 使用 #[proc_macro_attribute]
,编译期间会打印结构类型和参数,后面可用修改替换原属性定义。
Attr:struct, "world" | |
Item:struct Hello { pub name : String, } | |
Attr:func, "test" | |
Item:fn invoked() { } |
#[derive (Hello)] 使用 #[proc_macro_derive(Hello)]
・,会打印当前 TokenStream 结点流,可以和 syn 与 quto 库结合,扩展定义。
TokenStream [Ident { ident: "struct", span: #0 bytes(286..292) }, Ident { ident: "World", span: #0 bytes(293..298) }, Punct { ch: ';', spacing: Alone, span: #0 bytes(298..299) }] |