楼层: 首页/ 软件技术/ Rust 语言基础/ 宏与元编程:编译期就跑完的代码工厂
16

宏与元编程:编译期就跑完的代码工厂

Macros · macro_rules! · Procedural Macros

你从第一章就在用宏——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:列出几条匹配分支,每条分支写"长这样的输入 → 展开成这样的代码"。

// 三条分支:无参 / 一个参数 / 任意多个参数 macro_rules! greet { () => { println!("Hello!"); }; ($name:expr) => { println!("Hello, {}!", $name); }; ($($arg:expr),+ $(,)?) => { println!("你传了 {} 个参数", $($arg),+); }; } fn main() { greet!(); // 命中第一条 greet!("小明"); // 命中第二条 greet!(1, 2, 3); // 命中第三条:$($arg),+ 重复展开 }
$ cargo run Hello! Hello, 小明! 你传了 3 个参数
macro_rules! 里最常用的"重复片段"语法
写法含义
$x:expr捕获一个表达式,起名叫 x
$x:ident捕获一个标识符(变量名/类型名)
$x:ty捕获一个类型
$(...),*匹配零个或多个,用逗号分隔(对应 vec! 的 [1,2,3])
$(...),+匹配一个或多个(至少一个)
$(...)?可选片段(如末尾多余的逗号)
展开里再写 $(...),*把捕获到的片段逐个展开成代码
// 手搓一个简化版 vec!:理解 $()* 是怎么"循环展开"的 macro_rules! my_vec { ( $( $x:expr ),* ) => {{ let mut v = Vec::new(); $( v.push($x); )* // 这一行在展开时会"复制"成 N 行 push v }}; } fn main() { let v = my_vec![10, 20, 30]; println!("{:?}", v); // [10, 20, 30] }
$ cargo run [10, 20, 30]

过程宏:真正"读懂"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"),像函数但能读字符串里的 SQLsqlx 编译期 SQL 检查
// 写过程宏要开一个单独的 proc-macro crate,依赖 syn + quote // Cargo.toml: syn = { version = "2", features = ["full"] }, quote = "1" use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; // 一个最小的 derive 宏:给类型加一个 hello() 方法 #[proc_macro_derive(Hello)] pub fn derive_hello(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; // 拿到结构体名字 let expanded = quote! { impl #name { pub fn hello() { println!("你好,我是 {}", stringify!(#name)); } } }; expanded.into() } // 使用方: // #[derive(Hello)] struct Dog; Dog::hello();
宏的坑与最佳实践

① 宏会让编译变慢、报错变难读——展开后的代码报错能把你绕晕,先用 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 编译期检查)的时候。