宏与函数的本质区别(理解场景的前提)
| 维度 | 函数 | 宏 |
|---|---|---|
| 执行时机 | 运行期执行,参数是运行时值,类型固定 | 编译期展开,输入是语法树(token/抽象语法),能操作代码结构 |
| 类型约束 | 参数必须是确定类型,泛型函数仍受类型系统、生命周期、Trait 约束 | 不限制输入语法,可以接收任意代码片段、标识符、类型、语句块 |
| 作用域与语法访问 | 函数内部无法获取调用处的标识符、行号、文件名、局部变量名 | 宏可以捕获调用上下文全部语法信息 |
| 代码生成能力 | 函数只能执行逻辑、返回值,不能生成新结构体、impl、match 分支、常量 | 宏可以批量生成大量重复代码,消除模板冗余 |
核心结论:只要需求需要操作「代码本身、编译期信息、动态生成语法结构」,函数无法胜任,必须用宏。
二、场景 1:捕获编译期元信息(文件、行号、模块路径)
需求特征
日志、断言、错误追踪,需要打印代码所在文件名、行号、列号、模块路径。
函数做不到的原因
函数参数只能传运行时值,调用函数时无法自动把file!()line!()注入,必须手动传,极其繁琐。
宏实现(标准库示例assert!dbg!)
// 宏实现,自动捕获上下文 |
macro_rules! my_assert { |
($cond:expr, $msg:literal) => { |
if !$cond { |
panic!( |
"断言失败:{} \n文件:{} 行:{}", |
$msg, file!(), line!() |
) |
} |
}; |
} |
// 使用,无需手动传文件行号 |
my_assert!(1 + 1 == 3, "加法出错"); |
如果改用函数:
fn my_assert_func(cond: bool, msg: &str, file: &str, line: u32) { |
if !cond { panic!("{} {}:{}", msg, file, line); } |
} |
// 每次调用都要手动附加元信息,冗余爆炸 |
my_assert_func(1 + 1 == 3, "加法出错", file!(), line!()); |
典型标准库宏
dbg!、assert!/debug_assert!、todo!、unreachable!、panic!
三、场景 2:可变数量参数(任意个表达式、无固定签名)
需求特征
格式化打印、批量收集表达式、多参数日志,参数个数不固定。
函数局限
Rust 函数不支持真正可变参数:
- 只能用数组/vec 传参,需要手动包裹
vec![a, b, c] - 无法直接接收零散表达式,语法累赘
宏优势
宏可通过$(...),*匹配任意数量输入 token,原生支持变长参数。
// 简易 println 复刻宏 |
macro_rules! print_log { |
($($arg:expr),*) => { |
println!("{}", format!($($arg),*)); |
}; |
} |
// 任意个参数直接传入,不用容器包裹 |
print_log!("num={}", 123, ", str={}", "test"); |
函数方案对比(极其啰嗦):
fn print_log_func(args: &[&dyn std::fmt::Display]) { |
for a in args { print!("{}", a); } |
} |
print_log_func(&[&"num=", &123, &", str=", &"test"]); |
典型场景
日志库、格式化输出、批量求值宏。
四、场景 3:生成新语法结构(批量生成代码)
需求特征
批量生成结构体、枚举、impl 实现、常量、match 分支、测试用例。
函数完全不可能做到:函数运行时无法新增代码定义。
示例 1:批量定义常量
macro_rules! define_consts { |
($($name:ident = $val:expr),*) => { |
$( |
const $name: u32 = $val; |
)* |
}; |
} |
// 一行生成多个常量 |
define_consts!(A = 1, B = 2, C = 3); |
示例 2:批量实现 trait
trait Show { |
fn show(&self); |
} |
macro_rules! impl_show { |
($($ty:ty),*) => { |
$( |
impl Show for $ty { |
fn show(&self) { |
println!("值: {:?}", self); |
} |
} |
)* |
}; |
} |
// 一次性给多个类型实现 trait |
impl_show!(u8, u16, i32, String); |
典型使用场景
- 绑定 FFI C 枚举/结构体
- 数据库 ORM 批量生成模型代码
- 测试框架批量生成测试函数
- 状态机批量生成 match 分支
五、场景 4:操作标识符(变量名、类型名、函数名)
需求特征
动态拼接标识符、基于输入名字生成新变量/函数/字段。
函数完全无法实现:函数只能操作值,不能操作「变量名字符串标识符」,标识符是编译期语法概念,运行时不存在。
宏示例:拼接标识符
macro_rules! make_pair { |
($name:ident, $val:expr) => { |
// 拼接 ident:生成 xxx_val 变量 |
let concat_id = stringify!($name); |
let $name = $val; |
paste::paste! { |
let [<$name _val>] = $val * 2; |
println!("{}_val = {}", concat_id, [<$name _val>]); |
} |
}; |
} |
make_pair!(num, 10); |
// 展开后生成 num 和 num_val 两个局部变量 |
常见依赖:paste宏库用于标识符拼接。
业务场景
- 自动生成 get/set 方法
- 解析配置自动生成对应变量
- 解析协议字段自动生成访问器
六、场景 5:接收语法块(任意语句、match、loop、impl 等完整代码)
需求特征
自定义 DSL(领域特定语言)、封装执行上下文、作用域守卫、异步块包装。
