宏与元编程:编译期就跑完的代码工厂
你从第一章就在用宏——println!、vec!、format!、assert! 末尾那个感叹号就是在喊宏。这一章讲清楚宏到底是什么、怎么自己写一个,以及更强大的过程宏。这章要学:宏 vs 函数、macro_rules! 的匹配与展开、三种过程宏、什么时候真该用宏。
引子:为什么 vec! 是宏而不是函数
论函数干不到的事,宏来干
函数只能接收固定个数、固定类型的参数。可 vec![1, 2, 3, 4, 5] 想传几个传几个,println!("{} {}", a, b) 想打几个打几个。这俩能力普通函数给不了,所以 Rust 用宏:宏在编译期做文本展开,把 vec![1,2,3] 先翻译成一大段普通 Rust 代码,再交给编译器。你看到的感叹号 !,就是"这是宏调用"的标志。
macro_rules!:用"模式匹配"写宏
Rust 的声明宏长在 macro_rules! 上,写法像 match:列出几条匹配分支,每条分支写"长这样的输入 → 展开成这样的代码"。
| 写法 | 含义 |
|---|---|
$x:expr | 捕获一个表达式,起名叫 x |
$x:ident | 捕获一个标识符(变量名/类型名) |
$x:ty | 捕获一个类型 |
$(...),* | 匹配零个或多个,用逗号分隔(对应 vec! 的 [1,2,3]) |
$(...),+ | 匹配一个或多个(至少一个) |
$(...)? | 可选片段(如末尾多余的逗号) |
展开里再写 $(...),* | 把捕获到的片段逐个展开成代码 |
过程宏:真正"读懂"Rust 代码的宏
macro_rules! 只是按文本模式展开,看不懂 Rust 语法树。过程宏则能拿到完整的语法树(AST),能做更猛的事。标准库里的 #[derive(Debug)]、Axum 的 #[derive(Serialize)]、sqlx::query!() 全是过程宏。
| 种类 | 长什么样 | 谁在用 |
|---|---|---|
| 自定义 derive | #[derive(MyMacro)],自动给结构体/trait 实现代码 | serde::Serialize、thiserror、Debug |
| 属性宏 | #[route("GET", "/users")],包在函数上改它的行为 | Axum 的路由、tokio::main |
| 函数类宏 | sqlx::query!("SELECT 1"),像函数但能读字符串里的 SQL | sqlx 编译期 SQL 检查 |
① 宏会让编译变慢、报错变难读——展开后的代码报错能把你绕晕,先用 cargo expand(cargo-expand 工具)看展开后的真容。② 能用函数就别用宏:宏没有类型检查、不能当值传,是最后的武器。③ 过程宏要单独建 crate,主 crate 里 [lib] proc-macro = true。④ 业务代码 90% 的场景只需要会用现成宏(derive!、query!、route!),自己写宏一般在写库或框架时才需要。
1.(思考题)println! 末尾的感叹号能不能去掉?去掉会怎样?
查看答案
不能。println! 是宏,感叹号是宏调用的标志;去掉 println 编译器会当成一个叫 println 的函数去找,根本没有。看到 ! 就知道在编译期展开。
2.(代码填空)想让 my_vec![1,2,3] 支持"零个参数"(空 vec),重复运算符该用 ,* 还是 ,+?
查看答案
用 ,*。* 表示零个或多个,+ 表示至少一个。要支持空列表就得用 *。
3.(选择题)#[derive(Debug)] 属于哪一种过程宏?
A. 属性宏 B. 自定义 derive 宏 C. 函数类宏
查看答案
B。derive 宏自动给类型实现 trait(这里是 Debug);属性宏是 #[route(...)] 那种包在函数上的;函数类宏是 query!("...") 那种像函数调用的。
① 宏 = 编译期文本展开,能做函数做不到的"可变参数、格式校验、自动生成代码"。
② macro_rules! 用模式匹配写,$()* 负责重复展开;过程宏分 derive / 属性 / 函数类三种,能读懂语法树。
③ 优先用函数,宏是最后的武器;写库/框架时才需要自己造宏。
论费曼三问:用自己的话讲给外行听
① 宏和函数到底差在哪? 函数运行时跑、参数固定;宏编译期展开、参数个数和形式都可变。
② vec![1,2,3] 到底变成了什么? 先 let mut v = Vec::new(),再 v.push(1); v.push(2); v.push(3);,最后返回 v。
③ 什么时候你真要自己写宏? 当你要写一个库/框架,想让用户少写重复样板代码(比如自动实现 trait、把字符串里的 SQL 编译期检查)的时候。