Rust高级之宏讲解
1 宏
1.1 简介
宏(Macros)是一种在编译时生成代码的强大工具,它允许在编写代码时创建自定义语法扩展。宏(Macro)是一种在代码中进行元编程(Metaprogramming)的技术,允许在编译时生成代码,宏可以帮助简化代码,提高代码的可读性和可维护性,同时允许开发者在编译时执行一些代码生成的操作。
宏在 Rust 中有两种类型:声明式宏(Declarative Macros)和过程宏(Procedural Macros)。
1.2 声明式宏
1.2.1 定义
在 Rust 中,使用 macro_rules! 关键字来定义声明式宏。
macro_rules! my_macro {
($arg:expr) => {// 模式匹配和展开
// 生成的代码
// 使用 $arg 来代替匹配到的表达式
};
}
声明式宏使用 macro_rules! 关键字进行定义,它们被称为 macro_rules 宏,这种宏的定义是基于模式匹配的,可以匹配代码的结构并根据匹配的模式生成相应的代码。这样的宏在不引入新的语法结构的情况下,可以用来简化一些通用的代码模式。
片段说明符:
$arg:expr 里的 expr不是随便写的名字,是预定义的语法类别:
macro_rules! demo {
($e:expr) => { ... }; //匹配任意表达式:1+2, foo(), x
($i:ident) => { ... }; //匹配标识符:变量名、函数名
($t:ty) => { ... }; //匹配类型:i32, Vec<String>
($b:block) => { ... }; //匹配代码块:{ ... }
($s:stmt) => { ... }; //匹配语句
($p:pat) => { ... }; //匹配模式:Some(x), (a, b)
($l:literal) => { ... }; //匹配字面量:42, "abc"
($v:vis) => { ... }; //匹配可见性:pub, pub(crate)
($m:meta) => { ... }; //匹配属性内容:derive(Debug)
}
宏的重复语法:$(模式) 分隔符 重复次数,这里得分隔符是 重复单元分隔,是可选的
其中重复次数:
*:重复0次或多次+:重复1次或多次?:出现0或1次
1.2.2 多规则模式
多规则模式(类似 match):宏可以写多条规则,按顺序匹配,思想像 match 而不是正则
macro_rules! calc {
(add $a:expr, $b:expr) => { $a + $b }; //规则1:字面 token add +两表达式
(mul $a:expr, $b:expr) => { $a * $b }; //规则2
}
let x = calc!(add1,2); //展开为1 +2let y = calc!(mul3,4); //展开为3 *4
这里的 add 是自定义的字面 token,宏模式里不带 $前缀的标识符就是字面 token,多规则之间互相区分的唯一手段,调用宏时必须原样写出这个词,才能匹配上这条规则:calc!(add ...)
如果去掉了字面token 后,mul那条规则就永远失效
macro_rules! calc {
($name:expr) => { println!("Hello, {}!", $name) };
($a:expr, $b:expr) => { $a + $b }; //规则2
($c:expr, $d:expr) => { $c * $d }; //规则3:死代码!
}
calc!(3,4); //想让它做乘法?没门 →结果永远是7(加法)
因为宏的规则匹配是从上往下,第一个匹配上的就生效,后面不再尝试。规则2和规则3的模式一模一样(都是"表达式,表达式"),任何两个表达式的调用都会被规则2拦截,规则3这辈子都轮不到
字面token的真正作用:让调用方能选路
macro_rules! calc {
($name:expr) => { println!("Hello, {}!", $name) };
(add $a:expr, $b:expr) => { $a + $b };
(mul $a:expr, $b:expr) => { $a * $b };
}
calc!(1); //走规则1:Hello,1!
calc!(add 3,4); //走规则2:7
calc!(mul 3,4); //走规则3:12 ←靠 mul这个词区分出来
1.2.3 例子分析
1.2.3.1 简单例子
下面是一个简单的宏定义的例子:
// 宏的定义
macro_rules! greet {
// 模式匹配
($name:expr) => {
// 宏的展开
println!("Hello, {}!", $name);
};
}
fn main() {
// 调用宏
greet!("World");
}
说明
- 模式匹配:宏通过模式匹配来匹配传递给宏的代码片段,模式是宏规则的左侧部分,用于捕获不同的代码结构。
- 规则:宏规则是一组由
$引导的模式和相应的展开代码,规则由分号分隔。 - 宏的展开:当宏被调用时,匹配的模式将被替换为相应的展开代码,展开代码是宏规则的右侧部分。
1.2.3.2 宏重复示例
下面是一个更复杂的例子,演示了如何使用宏创建一个简单的 vec! 宏,以便更方便地创建 Vec:
// 宏的定义
macro_rules! vec {
// 基本情况,空的情况
() => {
Vec::new()
};
// 一次性的重复展开
// 表达式用逗号分隔 +表示至少1次 尾逗号:?表示0或1次
($($element:expr),+ $(,)?) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($element);
)+
temp_vec
}
};
}
fn main() {
// 调用宏
let my_vec = vec![1, 2, 3];
println!("{:?}", my_vec); // 输出: [1, 2, 3]
let empty_vec:Vec<i32> = vec![];
println!("{:?}", empty_vec); // 输出: []
}
在这个例子中,vec! 宏使用了模式匹配,以及 $($element:expr),+ $(,)? 这样的宏重复语法来捕获传递给宏的元素,并用它们创建一个 Vec。$(,)? 用于处理末尾的逗号,也是宏重复语法
1.2.4 输出宏 println!
println! 格式输出的命令:print!("hello,{}",a),可以在 {} 之间可以放一个数字,它将把之后的可变参数当作一个数组来访问,下标从 0 开始:println!("a is {0}, a again is {0}", a);
格式字符串中通过 {{ 和 }} 分别转义代表 { 和 }。但其他常用转义字符与 C 语言里的转义字符一样,都是反斜杠 \ 开头的形式
- 基础输出
{}:标准输出(Display格式)
{:p}:用于输出内存地址(指针)
{:?}:用于调试,会以开发者友好的方式打印出数据结构内部的值。要使用它,自定义类型(如结构体)必须实现Debug trait(通常通过在结构体上方加#[derive(Debug)]来自动实现)
{:#?}和{:?}一样用于调试,但会自动换行和缩进,打印复杂结构体或数组时非常清晰易读。 - 进制转换(数字专属),如果加上
#会带上进制前缀,比如{:#b}会输出 0b1010
{:b}:二进制(如 1010)
{:o}:八进制(如 12)
{:x}:小写十六进制(如 a)
{:X}:大写十六进制(如 A) - 对齐与填充(常用于打印表格)
{:<10}:左对齐,总宽度占 10 个字符,不足补空格。
{:>10}:右对齐,总宽度占 10 个字符。
{:^10}:居中对齐。
{:0>5}:右对齐,不足 5 位用数字 0 填充(比如 42 会变成 00042)。 - 精度控制
{:.2}:浮点数保留 2 位小数(如 3.14)。
{:.3}:如果是字符串,则只截取前 3 个字符。
1.3 过程宏
1.3.1 简介
过程宏是一种更为灵活和强大的宏,允许在编译时通过自定义代码生成过程来操作抽象语法树(AST)。过程宏在功能上更接近于函数,但是它们在编写和使用上更加复杂。
过程宏的类型:
| 类型 | 用法形态 | 作用 |
|---|---|---|
| 派生宏(Derive) | #[derive(MyTrait)] |
自动为结构体实现 trait |
| 属性宏(Attribute) | #[my_attr] |
改造/包装被标注的代码 |
| 函数式宏(Function-like) | my_macro!(...) |
长得像声明宏,但内部是代码生成 |
这里需要在项目的 Cargo.toml 添加如下:
[lib]
proc-macro = true
过程宏入参 input: TokenStream 是一段原始 token流(类似未解析的字符串),无法直接用,需要 parse_macro_input! 把它解析成结构化的 DeriveInput,才能使用
DeriveInput 结构体有几个核心字段:
| 字段 | 含义 | 示例 |
|---|---|---|
ident |
类型名称标识符 | User |
data |
内容(是 struct/enum/union) | Data::Struct(...) |
attrs |
属性列表 | #[derive(...)] |
generics |
泛型参数 | <T> |
1.3.2 派生宏
派生宏(Derive Macro)是过程宏中最常见的一种,它允许通过 #[derive(MyTrait)] 属性自动为结构体或枚举实现特定的 trait。Rust 标准库中的 Debug、Clone、PartialEq 等 trait 就是通过派生宏实现的。自定义派生宏可以极大地减少重复代码,提升开发效率。
1.3.2.1 定义
- 定义位置:派生宏必须在独立的
crate中定义,且该crate的Cargo.toml中需声明[lib] proc-macro = true。 - 函数签名:派生宏的函数接收一个
TokenStream作为输入(即#[derive(...)]所标注的代码),并返回一个新的TokenStream(即生成的代码)。 - 关键库:
syn用于将TokenStream解析为结构化的 Rust 语法树(AST)而quote是用于将 Rust 代码模板转换回TokenStream,支持变量插值(#variable)。
1.3.2.2 简单例子分析
我们将创建一个派生宏 HelloDebug,它不仅实现 Debug trait,还会在输出中包含结构体名称和每个字段的名称与值。
定义宏的 Crate (hello_macro),首先,创建一个新的库 crate:
cargo new hello_macro --lib
编辑 hello_macro/Cargo.toml:
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
2. 实现派生宏 (hello_macro/src/lib.rs)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data};
#[proc_macro_derive(HelloDebug)]
pub fn hello_debug_derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = parse_macro_input!(input);// 1. 解析输入为 DeriveInput 结构体
// 2. 提取结构体/枚举的名称
let name = &ast.ident;
// 3. 根据数据类型(结构体、枚举、联合体)生成不同的匹配代码
let debug_impl = match ast.data {
Data::Struct(ref data_struct) => {
// 处理具名字段的结构体 (struct Point { x: i32, y: i32 })
if let syn::Fields::Named(ref fields) = data_struct.fields {
let field_idents: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
let field_names: Vec<String> = field_idents.iter().map(|ident| ident.as_ref().unwrap().to_string()).collect();
quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
// 动态生成字段输出
let mut debug_struct = f.debug_struct(stringify!(#name));
#(
debug_struct.field(#field_names, &self.#field_idents);
)*
debug_struct.finish()
}
}
}
} else {
// 处理元组结构体或无字段结构体(简化处理)
quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "HelloDebug {{ type: {} }}", stringify!(#name))
}
}
}
}
}
Data::Enum(_) | Data::Union(_) => {
// 对于枚举和联合体,提供一个基础实现(可根据需要扩展)
quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "HelloDebug(enum/union): {}", stringify!(#name))
}
}
}
}
};
// 4. 将生成的代码转换回 TokenStream 并返回
TokenStream::from(debug_impl)
}
代码解析:
parse_macro_input!:将原始的TokenStream解析为DeriveInput,方便我们访问结构体的名称、字段等信息。ast.ident:获取被标注的类型名称(例如Point)。quote!宏:用于编写代码模板。在模板中,#name会被替换为实际的类型名,#(...)*是重复展开语法,这里是模仿重复宏语法是quote!特有的重复语法,用于为每个字段生成对应的debug_struct.field(...)调用。- 匹配
ast.data:根据类型是结构体、枚举还是联合体,生成不同的Debug实现。这里重点展示了如何处理具名字段的结构体。
3. 使用派生宏
在另一个项目(例如 main crate)中,添加依赖并使用:
[dependencies]
hello_macro = { path = "../hello_macro" }
use hello_macro::HelloDebug;
#[derive(HelloDebug)]
struct Point {
x: i32,
y: i32,
}
#[derive(HelloDebug)]
struct User {
id: u64,
name: String,
active: bool,
}
fn main() {
let p = Point { x: 10, y: 20 };
let u = User { id: 1, name: "Alice".to_string(), active: true };
println!("{:?}", p); // 输出: Point { x: 10, y: 20 }
println!("{:?}", u); // 输出: User { id: 1, name: "Alice", active: true }
}
1.3.2.3 复杂例子:生成 Builder 模式
派生宏还可以用于自动生成构建器(Builder)模式代码,这是一个非常实用的场景。
// 在 hello_macro crate 中定义另一个派生宏
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let builder_name = syn::Ident::new(&format!("{}Builder", name), name.span());
// 假设只处理具名字段的结构体
let fields = if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(ref fields), .. }) = ast.data {
&fields.named
} else {
panic!("Builder derive only supports structs with named fields");
};
// 为每个字段生成 setter 方法
let setter_methods = fields.iter().map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
quote! {
pub fn #field_name(mut self, value: #field_type) -> Self {
self.#field_name = Some(value);
self
}
}
});
// 生成 Builder 结构体的字段(每个都是 Option)
let builder_fields = fields.iter().map(|f| {
let field_name = &f.ident;
let field_type = &f.ty;
quote! {
#field_name: std::option::Option<#field_type>
}
});
// 生成 build 方法,用于最终构建目标结构体
let build_assignments = fields.iter().map(|f| {
let field_name = &f.ident;
quote! {
#field_name: self.#field_name.ok_or(format!("field `{}` not set", stringify!(#field_name)))?
}
});
let expanded = quote! {
// 定义 Builder 结构体
struct #builder_name {
#(#builder_fields,)*
}
impl #builder_name {
#(#setter_methods)*
pub fn build(self) -> Result<#name, String> {
Ok(#name {
#(#build_assignments,)*
})
}
}
// 为原结构体实现 builder() 方法
impl #name {
pub fn builder() -> #builder_name {
#builder_name {
#(
#field_name: None,
)*
}
}
}
};
TokenStream::from(expanded)
}
使用 Builder 派生宏:
#[derive(Builder)]
struct Config {
timeout: u32,
retries: u8,
url: String,
}
fn main() {
let config = Config::builder()
.timeout(30)
.retries(3)
.url("https://example.com".to_string())
.build()
.unwrap();
println!("Config built: {:?}", config);
}
1.3.3 属性宏
// 定义
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
// attr: "/hello"
// item: fn hello() { ... }
let path = attr.to_string();
let func = parse_macro_input!(item as syn::ItemFn); //func =整个函数的结构化表示
//let func_name = &func.sig.ident;//只是函数名这一个标识符
let route_fn_name = format_ident!("{}_route_path", func.sig.ident);
let expanded = quote! {
#func // 保留原函数
// 额外生成注册代码 为了 让框架在运行时能找到路由路径:
fn #route_fn_name () -> &'static str {
#path
}
};
TokenStream::from(expanded)
}
使用:
#[route("/hello")]
fn hello() -> &'static str {
"Hello, World!"
}
1.3.4 函数式宏
use syn::{parse_macro_input, LitStr};
// 定义
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
// 解析 SQL 字符串,生成类型安全的查询代码
let lit = parse_macro_input!(input as LitStr); //要求输入是字符串字面量
let sql_str = lit.value(); //拿到纯文本,不带引号
let expanded = quote! {
// 生成的代码
format!("执行 SQL: {}", #sql_str)
};
TokenStream::from(expanded)
}
使用:
let query = sql!(SELECT * FROM users WHERE id = 1);
// 编译时解析 SQL,生成对应代码

浙公网安备 33010602011771号