第 12 章 元编程:宏
第 12 章 元编程:宏(Macros)
“宏是 Rust 世界里的’哆啦A梦口袋’——你想吃什么(输入什么),它就能给你掏出来什么。函数做不到的事,宏能做到;函数能做的事,宏也能做。唯一的代价是——你需要花点时间学会跟它相处。”
在编程的世界里,“元编程”(Metaprogramming)是一个听起来很高大上的词汇。它本质上说的是:写一段代码,让这段代码去生成别的代码。
你可能会想:为什么要这么做?
好问题!想象一下,你要写一个 println!("Hello, {}! You are {} years old.", name, age); 这样的代码。如果每次都要手写这一长串,那不得累死?但是如果你有一个宏:
宏就会自动帮你展开成那串长长的代码。这就是元编程的魅力——一次定义,无数次使用。
这一章,我们就要学习 Rust 的宏系统。别担心,我会把它拆解得连你奶奶都能看懂(没有冒犯奶奶的意思)。
12.1 宏概述
12.1.1 宏 vs 函数的本质区别
12.1.1.1 宏在编译期展开,函数在运行时调用
这是宏和函数最核心的区别:
1
2
3
4
5
6
7
8
9
10
| // 宏:在编译期展开,代码直接替换到调用处
macro_rules! say_hi {
() => {
println!("Hi!");
};
}
fn main() {
say_hi!(); // 编译时,编译器会把这行替换成 println!("Hi!");
}
|
1
2
3
4
5
6
7
8
| // 函数:在运行时被调用
fn say_hi() {
println!("Hi!");
}
fn main() {
say_hi(); // 普通函数调用,不用 !
}
|
时间线对比:
gantt
title 编译 vs 运行
dateFormat X
axisFormat
section 编译期
宏展开 : 0, 5
section 运行期
函数执行 : done, 1012.1.1.2 宏可以生成语法(代码生成),函数不行
宏可以做到一些函数做不到的事:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| macro_rules! create_struct {
($name:ident) => {
struct $name {
value: i32,
}
};
}
create_struct!(MyStruct); // 展开成 struct MyStruct { value: i32 }
fn main() {
let s = MyStruct { value: 42 };
println!("{}", s.value); // 42
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 函数能返回类型吗?不能!
// fn create_struct<T>() { ... } // 做不到
// 但宏可以生成任意语法结构
macro_rules! impl_display {
($type:ident) => {
impl std::fmt::Display for $type {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "({}, {})", self.0, self.1)
}
}
};
}
impl_display!(Point);
struct Point(i32, i32);
fn main() {
let p = Point(3, 4);
println!("{}", p); // (3, 4)
}
|
12.1.1.3 宏可以操纵语法树( Hygiene / 不 Hygiene)
这个概念比较抽象,但我会尽量解释清楚。
Hygiene(卫生宏)指的是:宏展开后的代码不会跟外部代码产生命名冲突。
1
2
3
4
5
6
7
8
9
10
11
12
| macro_rules! hygiene_demo {
() => {
let x = 10;
println!("x = {}", x);
};
}
fn main() {
let x = 20; // 外部也有一个 x
hygiene_demo!(); // 宏内部定义的 x 不会跟外部冲突
println!("外部的 x = {}", x); // 仍然是 20
}
|
但 macro_rules! 有一些"不卫生"的情况(后面会讲)。
12.1.1.4 宏参数可以是标识符 / 表达式 / 类型(函数参数不行)
函数只能接受值作为参数,而宏可以接受语法元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| macro_rules! inspect_type {
// 接受一个类型
($t:ty) => {
println!("类型大小:{} bytes", std::mem::size_of::<$t>());
};
}
macro_rules! inspect_expr {
// 接受一个表达式
// 这里用 {:?} 而不是 {}:因为数组等类型没有实现 Display,只能用 Debug
($e:expr) => {
println!("表达式的值是:{:?}", $e);
};
}
macro_rules! inspect_ident {
// 接受一个标识符
($i:ident) => {
println!("标识符:{}", stringify!($i));
};
}
fn main() {
inspect_type!(i32); // 类型大小:4 bytes
inspect_expr!(2 + 2 * 3); // 表达式的值是:8
inspect_expr!([1, 2, 3]); // 表达式的值是:[1, 2, 3]
inspect_ident!(my_variable); // 标识符:my_variable
}
|
生活类比:宏就像一个万能厨师,它可以接收"切菜"、“炒菜”、“装盘"这样的指令,然后生成对应的代码。而函数只能接收"胡萝卜丝"这样的具体值。
12.1.2 声明宏(Declarative Macros)
12.1.2.1 macro_rules! 语法(基于模式匹配的编译期展开)
macro_rules! 是 Rust 中最常用的宏定义方式,叫做"声明宏”:
1
2
3
4
5
6
| macro_rules! my_macro {
// 模式 arm
($pattern) => {
$expansion
};
}
|
结构就是:匹配模式 → 展开代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| macro_rules! greet {
// 匹配没有参数的情况
() => {
println!("Hello, World!");
};
// 匹配单个字符串参数
($name:expr) => {
println!("Hello, {}!", $name);
};
// 匹配两个参数
($greeting:expr, $name:expr) => {
println!("{}, {}!", $greeting, $name);
};
}
fn main() {
greet!(); // Hello, World!
greet!("Rust"); // Hello, Rust!
greet!("Good morning", "Alice"); // Good morning, Alice!
}
|
12.1.2.2 声明宏的两种形式:item macro / expression macro
Item Macro:生成完整的 Rust 条目(struct、enum、fn 等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("函数 {} 被调用了!", stringify!($name));
}
};
}
create_function!(say_hello);
create_function!(say_goodbye);
fn main() {
say_hello(); // 函数 say_hello 被调用了!
say_goodbye(); // 函数 say_goodbye 被调用了!
}
|
Expression Macro:生成表达式
1
2
3
4
5
6
7
8
9
10
11
| macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
let n = 5;
println!("{} 的平方是 {}", n, square!(n)); // 5 的平方是 25
println!("3 + 4 的平方是 {}", square!(3 + 4)); // 3 + 4 的平方是 49
}
|
12.1.3 过程宏(Procedural Macros)
12.1.3.1 函数式宏(function-like macro)
看起来像函数调用,但以 ! 结尾,实际上是过程宏:
1
2
3
4
| // 这是标准库提供的宏,看起来像函数调用
println!("Hello, {}!", "world");
vec![1, 2, 3, 4, 5];
panic!("出错了!");
|
12.1.3.2 派生宏(#[derive(…)])
#[derive(...)] 是最常见的过程宏,用于自动实现 trait:
1
2
3
4
5
6
7
8
9
10
11
12
13
| #[derive(Debug, Clone, PartialEq, Eq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();
println!("{:?}", p1); // Point { x: 1, y: 2 }
println!("p1 == p2: {}", p1 == p2); // p1 == p2: true
}
|
#[derive] 宏自动帮你生成了 Debug、Clone、PartialEq 等 trait 的实现代码。
12.1.3.3 属性宏(#[attribute])
属性宏可以给任何条目附加额外的行为:
1
2
3
4
5
| #[my_attribute]
struct Foo { ... }
#[another_attribute]
fn bar() { ... }
|
预告:过程宏的详细内容会在后面的 12.3 节展开。
12.2 声明宏(macro_rules!)
12.2.1 基本语法与匹配规则
12.2.1.1 macro_rules! name { ($pattern) => { $expansion }; }
macro_rules! 的基本骨架就是「匹配器 → 展开器」:左边写模式,右边写替换后的代码。
1
2
3
4
5
6
| // 基本结构
macro_rules! macro_name {
( $pattern1 ) => { $expansion1 };
( $pattern2 ) => { $expansion2 };
// 更多 arm...
}
|
12.2.1.2 单分支宏
最简单的宏不带参数,调用时直接展开成固定的代码。
1
2
3
4
5
6
7
8
9
| macro_rules! say_bye {
() => {
println!("再见!");
};
}
fn main() {
say_bye!(); // 再见!
}
|
12.2.1.3 多分支宏(按顺序匹配)
多个分支按书写顺序逐个尝试,第一个匹配成功的被采用;宏只按语法形状匹配,不支持「匹配守卫」,要按值分支就得把值交给展开出的 if/match。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // ⚠️ 常见误区:macro_rules! 不支持 `($n:expr) if $n < 0 => ...` 这种「匹配守卫」。
// 宏只在编译期按「语法形状」匹配,不能判断运行时的值。
// 要按值分支,请直接写普通函数(或把值交给宏展开出的 match/if):
fn classify(n: i32) -> &'static str {
if n < 0 {
"负数"
} else if n == 0 {
"零"
} else {
"正数"
}
}
// 如果确实想用宏,可以让宏只负责接收表达式,再由展开出的代码做判断:
macro_rules! classify_or_other {
($n:expr) => {
if $n < 0 {
println!("{} 是负数", $n);
} else if $n == 0 {
println!("{} 是零", $n);
} else {
println!("{} 是正数", $n);
}
};
}
fn main() {
println!("{} 是负数", classify(-5)); // -5 是负数
println!("{} 是零", classify(0)); // 0 是零
println!("{} 是正数", classify(42)); // 42 是正数
classify_or_other!(-5);
classify_or_other!(0);
classify_or_other!(42);
}
|
注意:带 if 守卫(guard)的 arm 必须放在没有守卫的 arm 后面!这是因为 Rust 宏按顺序匹配——没有 guard 的 arm 会匹配一切,把没有 guard 的 arm 放在前面会导致带 guard 的 arm 永远不会被执行。想象一下:相亲的时候,你跟红娘说"先帮我筛选一下低于 30 岁的",结果红娘直接把所有人的资料都给你了,说"这些都符合你的要求"——你的 guard 条件直接被无视了。
12.2.2 重复模式
12.2.2.1 $(…)* 零次或多次
$( ... )* 表示重复零次或多次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| macro_rules! print_all {
// $( $item:expr )* 会匹配零个或多个表达式
( $( $item:expr ),* ) => {
// 展开时用 $( ... )* 来重复
$(
println!("{}", $item);
)*
};
}
fn main() {
print_all!(1, 2, 3); // 1\n2\n3
print_all!("a", "b"); // a\nb
print_all!(); // 什么都不打印
}
|
12.2.2.2 $(…)+ 一次或多次(至少一次)
$( ... )+ 表示至少重复一次,所以 sum!() 这样的空调用会直接报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
| macro_rules! sum {
( $( $n:expr ),+ ) => {
// 模式中的 + 表示至少匹配一个表达式
// 展开时用 $( + $n )* 从第二个开始每个前面加 +
0 $( + $n )*
};
}
fn main() {
println!("1+2 = {}", sum!(1, 2)); // 1+2 = 3
println!("1+2+3+4 = {}", sum!(1, 2, 3, 4)); // 1+2+3+4 = 10
// sum!() 编译错误!因为 + 要求至少一个
}
|
12.2.2.3 $(…)? 零次或一次
$( ... )? 表示零次或一次,常用来匹配可选的尾随逗号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| macro_rules! optional_debug {
($name:expr, debug: $($key:ident),*) => {
{
print!("{}: {:?}", stringify!($name), $name);
$(
print!(", {}: {:?}", stringify!($key), $key);
)*
println!();
}
};
($name:expr) => {
println!("{}", $name);
};
}
fn main() {
let x = 42;
let y = "hello";
optional_debug!(x); // 42
optional_debug!(x, debug: x, y); // x: 42, x: 42, y: "hello"
}
|
12.2.3 捕获片段
12.2.3.1 $name:expr(表达式)
$name:expr 匹配一个表达式片段。
1
2
3
4
5
6
7
8
9
10
11
| macro_rules! eval {
($e:expr) => {
println!("表达式: {:?} = {}", stringify!($e), $e);
};
}
fn main() {
eval!(1 + 2); // 表达式: 1 + 2 = 3
eval!(2 * 3 + 4); // 表达式: 2 * 3 + 4 = 10
eval!("hello"); // 表达式: "hello" = hello
}
|
12.2.3.2 $name:stmt(语句)
$name:stmt 匹配一条语句;注意 stmt 片段本身已经吃掉了结尾的分号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // ⚠️ stmt 片段本身已经「吃掉」了结尾的分号,所以重复子句里
// 不能再写分隔符 `;`,展开时也不要额外补分号。
macro_rules! run_statements {
( $( $s:stmt )* ) => {
$(
$s
)*
};
}
fn main() {
run_statements! {
let x = 10;
let y = 20;
println!("x + y = {}", x + y); // x + y = 30
}
}
|
12.2.3.3 $name:ty(类型)
$name:ty 匹配一个类型,常与 std::any::type_name 搭配做类型探针。
1
2
3
4
5
6
7
8
9
10
11
12
| macro_rules! type_info {
($t:ty) => {
println!("类型: {}", std::any::type_name::<$t>());
println!("大小: {} bytes", std::mem::size_of::<$t>());
};
}
fn main() {
type_info!(i32); // 类型: i32\n大小: 4 bytes
type_info!(String); // 类型: alloc::string::String\n大小: 24 bytes (在64位系统)
type_info!(Vec<u8>); // 类型: Vec<u8>\n大小: 24 bytes
}
|
12.2.3.4 $name:pat(模式)
$name:pat 匹配一个模式片段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| macro_rules! match_pattern {
($p:pat) => {
let x = 42;
match x {
$p => println!("匹配到了!"),
_ => println!("没匹配到..."),
}
};
}
fn main() {
match_pattern!(42); // 匹配到了!
match_pattern!(n); // 匹配到了!(n 是绑定模式,会匹配任何值,包括 42)
}
|
12.2.3.5 $name:ident(标识符)
$name:ident 匹配单个标识符;因为宏可以「凭名字造东西」,它常用来生成变量名或函数名。
1
2
3
4
5
6
7
8
9
10
| macro_rules! create_var {
($name:ident, $value:expr) => {
let $name = $value;
};
}
fn main() {
create_var!(my_number, 100);
println!("my_number = {}", my_number); // my_number = 100
}
|
12.2.3.6 $name:block(代码块)
$name:block 匹配一对大括号包起来的代码块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| macro_rules! time_it {
($block:block) => {{
let start = std::time::Instant::now();
let result = $block;
let elapsed = start.elapsed();
println!("耗时: {:?}", elapsed);
result
}};
}
fn main() {
// ⚠️ $block:block 匹配的是「带花括号的一块」,所以调用时要写成 ({ ... })
let result = time_it!({
let mut sum = 0;
for i in 0..1000000 {
sum += i;
}
sum
});
println!("结果: {}", result); // 结果: 499999500000
}
|
$name:meta 匹配属性方括号里的内容,适合写接收 #[...] 的宏。
1
2
3
4
5
6
7
8
9
10
11
12
13
| macro_rules! with_attrs {
( $( #[$attr:meta] )* $item:item ) => {
$(
println!("属性: {:?}", stringify!($attr));
)*
$item
};
}
#[with_attrs]
#[derive(Debug)]
struct Point(i32, i32);
// 输出: 属性: Derive(Debug)
|
12.2.3.8 $name:tt(TokenTree,任意 token 树)
$name:tt 匹配一棵 token 树;单个 tt 只吃一个 token,要匹配任意多个请用 $($tt:tt)*。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ⚠️ 单个 $tt 只能匹配「一个」token tree,写 dump_tt!(a + b * c) 会报
// 「no rules expected `+`」。要匹配任意多个,请用 $($tt:tt)*。
macro_rules! dump_tt {
($($tt:tt)*) => {
println!("TokenTree: {:?}", stringify!($($tt)*));
};
}
fn main() {
dump_tt!(foo); // TokenTree: foo
dump_tt!(a + b * c); // TokenTree: a + b * c
dump_tt!([1, 2, 3]); // TokenTree: [1, 2, 3]
}
|
12.2.3.9 $name:path(路径,如 std::vec::Vec)
$name:path 匹配一条路径;它后面不能再直接跟 <u32> 或 ::,需要时改用 ty 或 ident 片段。
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ⚠️ $path:path 后面不能再直接跟 `<u32>`,也不能再跟 `::`(宏解析会失败)。
// 类型用 $t:ty,把「类型名当函数调用」的那部分用 $c:ident。
macro_rules! instantiate {
($t:ty, $c:ident) => {{
let v: $t = $c::new();
v
}};
}
fn main() {
let vec: Vec<u32> = instantiate!(Vec<u32>, Vec);
println!("空 Vec: {:?}", vec); // 空 Vec: []
}
|
12.2.3.10 $name:literal(字面量)
$name:literal 匹配字面量(数字、字符串、字符、布尔值)。
1
2
3
4
5
6
7
8
9
10
11
| macro_rules! check_literal {
($lit:literal) => {
println!("字面量: {:?}", $lit);
};
}
fn main() {
check_literal!(42); // 字面量: 42
check_literal!("hello"); // 字面量: "hello"
check_literal!('c'); // 字面量: 'c'
}
|
12.2.4 卫生性(Hygiene)
12.2.4.1 卫生宏的概念(宏展开的标识符不影响周围代码)
卫生宏是 Rust 宏的一个"保镖"特性——它确保宏展开的代码不会意外污染周围的作用域。
1
2
3
4
5
6
7
8
9
10
11
12
| macro_rules! hygienic {
() => {
let my_secret = 42; // 这个变量不会跟外部冲突
};
}
fn main() {
let my_secret = 100; // 外部也有一个 my_secret
hygienic!();
// my_secret 仍然是 100,不会被宏覆盖
println!("{}", my_secret); // 100
}
|
12.2.4.2 卫生宏的标识符解析(宏内部的变量名不与外部冲突)
宏是「卫生」的:宏内部定义的变量不会与调用处的同名变量冲突,展开后不会互相踩到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| macro_rules! safe_add {
($a:expr, $b:expr) => {{
let temp_a = $a;
let temp_b = $b;
temp_a + temp_b
}};
}
fn main() {
let temp_a = 999; // 外部也有 temp_a
let result = safe_add!(1, 2);
println!("result = {}", result); // result = 3
println!("temp_a = {}", temp_a); // temp_a = 999,没被影响!
}
|
12.2.4.3 不卫生的情况(macro_rules! 中的特殊变量)
有一些特殊情况会导致"不卫生":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| macro_rules! non_hygienic {
($var:ident) => {
// 这里使用 $var 时,它会捕获外部的同名变量
macro_rules! inner {
() => { $var }
}
};
}
fn main() {
let x = 10;
non_hygienic!(x);
println!("{}", inner!()); // 10(捕获了外部的 x)
}
|
12.2.4.4 $crate 宏路径(宏内部访问 crate 内部路径)
$crate 是一个特殊的变量,它指向当前 crate 的根:
1
2
3
4
5
6
7
8
9
| // 在 lib.rs 中定义宏
#[macro_export]
macro_rules! use_types {
() => {
// 使用 $crate 来引用当前 crate 内部的类型
let v: $crate::MyType = $crate::MyType::new();
v
};
}
|
提示:#[macro_export] 将宏导出到 crate 根级别,这样外部代码可以用 use crate::macro_name; 或者 crate_name::macro_name! 来使用。
12.2.5 常见声明宏示例
12.2.5.1 vec![] 的简化实现
标准库的 vec![] 大致是这样实现的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| macro_rules! my_vec {
// vec![1, 2, 3]
( $( $elem:expr ),* $(,)? ) => {{
let mut v = Vec::new();
$(
v.push($elem);
)*
v
}};
// vec![value; count] - 用同一个值初始化多个元素
( $elem:expr; $count:expr ) => {{
let v: Vec<_> = std::iter::repeat($elem).take($count).collect();
v
}};
}
fn main() {
let v1 = my_vec![1, 2, 3];
println!("{:?}", v1); // [1, 2, 3]
let v2 = my_vec![0; 5];
println!("{:?}", v2); // [0, 0, 0, 0, 0]
}
|
12.2.5.2 println! 的简化实现(可变参数)
下面用几条规则模拟 println! 的可变参数:先处理无参数的情况,再处理带格式串与参数的调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| macro_rules! my_println {
// 匹配没有参数的情况
() => {
println!();
};
// 匹配有参数的情况
($($arg:tt)*) => {
// 直接转发给标准库的 println!
println!($($arg)*);
};
}
fn main() {
my_println!(); // 换行
my_println!("Hello"); // Hello
my_println!("Hello, {}", "World"); // Hello, World
my_println!("{} + {} = {}", 1, 2, 3); // 1 + 2 = 3
}
|
12.2.5.3 dbg! 的简化实现(调试输出)
dbg! 的简化版演示了「打印表达式本身和它的值,再把它原样返回」这套调试习惯。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| macro_rules! my_dbg {
// dbg!(expr)
( $expr:expr $(,)? ) => {
// std::dbg! 返回表达式的值,所以它可以被用在任何需要值的地方
match $expr {
value => {
eprintln!("[{}:{}] {} = {:?}",
file!(), line!(), stringify!($expr), &value);
value
}
}
};
}
fn main() {
let x = 5;
let y = 10;
my_dbg!(x + y); // [src/main.rs:6] x + y = 15
}
|
12.2.5.4 panic! 的简化实现
panic! 的简化版演示如何在宏里把消息转成 panic。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| macro_rules! my_panic {
// panic!("message")
( $msg:expr $(,)? ) => {
// 使用 std::panic::panic_any 来触发 panic
std::panic::panic_any(std::borrow::Cow::from($msg))
};
// panic!("format {}", args...)
( $fmt:expr, $( $args:expr ),* $(,)? ) => {
std::panic::panic_any(std::borrow::Cow::from(format!($fmt, $($args),*)))
};
}
fn main() {
// my_panic!("这是一个 panic!"); // 运行时会 panic
}
|
12.2.5.5 concat! / stringify! 的实现
concat!/stringify! 属于编译期字符串处理宏,下面用宏模拟它们的展开效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // concat! 在编译期拼接字符串(这里用运行时实现模拟效果)
macro_rules! my_concat {
( $( $s:expr ),* $(,)? ) => {{
// 模拟 concat! 的效果,实际的 concat! 是编译器内置的
let mut result = String::new();
$(
result.push_str(&$s.to_string());
)*
result
}};
}
// stringify! 把代码转成字符串
macro_rules! my_stringify {
( $( $t:tt )* ) => {
// stringify! 是编译器内置的,这里模拟它的效果
// 实际上 stringify! 是编译器直接处理的
concat!( $(stringify!($t)),* )
};
}
fn main() {
println!("{}", my_concat!("Hello", " ", "World")); // Hello World
println!("{}", my_stringify!(fn main() { let x = 1; })); // fn main() { let x = 1; }
}
|
12.3 过程宏(Procedural Macros)
12.3.1 过程宏的工作原理
12.3.1.1 TokenStream → TokenStream(输入 token 流,输出 token 流)
过程宏本质上是一个函数,它接收 TokenStream(Token 流),处理后输出另一个 TokenStream:
1
2
3
4
5
| // 过程宏函数签名
fn process(input: TokenStream) -> TokenStream {
// input: 输入的 TokenStream
// output: 输出的 TokenStream
}
|
12.3.1.2 proc_macro crate(定义宏的接口)
proc_macro crate 提供了编写过程宏的类型和函数:
1
2
3
4
5
6
7
8
| // lib.rs - 过程宏 crate
use proc_macro::TokenStream;
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// 你的宏逻辑
TokenStream::new()
}
|
12.3.1.3 proc_macro2 crate(跨平台 token 表示)
proc_macro2 是 proc_macro 的"可移植版本",通常配合 syn 和 quote 使用:
1
2
3
4
5
| # Cargo.toml
[dependencies]
proc-macro2 = "1.0"
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
|
12.3.2 #[derive(…)] 派生宏
12.3.2.1 #[proc_macro_derive(TraitName, attributes(name = “…”))]
定义一个派生宏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // lib.rs
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// 生成代码
let expanded = quote::quote! {
impl #name {
pub fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
expanded.into()
}
|
解析 DeriveInput 可以拿到结构体或枚举的名字、泛型参数和字段信息,具体形态由 Data 枚举区分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| use syn::{parse_macro_input, DeriveInput, Data, Generics};
#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match &input.data {
Data::Struct(data) => {
println!("这是一个结构体,有 {} 个字段",
data.fields.len());
}
Data::Enum(data) => {
println!("这是一个枚举,有 {} 个变体",
data.variants.len());
}
Data::Union(data) => {
println!("这是一个联合体");
}
}
TokenStream::new()
}
|
12.3.2.3 生成 trait 实现代码
拿到输入信息后,用 quote! 拼出要生成的 trait 实现代码,再交回给编译器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data};
#[proc_macro_derive(Display)]
pub fn derive_display(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// 只有结构体才有字段,这里简化处理
let fields = match &input.data {
Data::Struct(data) => {
&data.fields
}
_ => panic!("Display derive 只支持结构体"),
};
// 生成字段打印的代码
let field_outputs: Vec<_> = fields.iter().filter_map(|f| {
let field_name = f.ident.as_ref()?;
Some(quote! {
write!(f, "{} = {:?}, ", stringify!(#field_name), self.#field_name)?;
})
}).collect();
let expanded = quote! {
impl std::fmt::Display for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{} {{ ", stringify!(#name))?;
#(#field_outputs)*
write!(f, "}}")?;
Ok(())
}
}
};
expanded.into()
}
|
12.3.3 #[attribute] 属性宏
12.3.3.1 #[proc_macro_attribute]
#[proc_macro_attribute] 声明属性宏,它同时接收属性参数和它所修饰的那段代码。
1
2
3
4
5
6
7
8
9
10
| use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn my_attribute(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("属性参数: {:?}", attr.to_string());
println!("被修饰的项: {:?}", item.to_string());
// 可以修改 item 或者直接返回原样
item
}
|
12.3.3.2 属性宏的输入(TokenStream 属性 + TokenStream 项目)
属性宏的两个输入分别是属性括号里的内容和被修饰的代码,通常原样返回后者,中途插入生成的内容。
1
2
3
| // 使用方式
#[my_attribute(key = "value")]
struct Foo { ... }
|
12.3.3.3 路由宏(如 axum 的 #[axum::routes])
Web 框架里的路由属性宏就是典型例子:读出属性里的路径信息,再生成注册路由的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 简化版的路由宏
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let method = attr.to_string();
quote::quote! {
#[allow(non_camel_case_types)]
struct #method;
impl Route for #method {
fn handler() {
println!("处理 {} 请求", #method);
}
}
#item
}.into()
}
// 使用
#[route(GET)]
fn get_user() {
println!("获取用户信息");
}
|
12.3.4 内置属性
12.3.4.1 #[inline] / #[inline(always)] / #[inline(never)]
#[inline] 交给编译器自行决定,always/never 则是强提示;它们只是建议,不构成语义保证。
1
2
3
4
5
6
7
8
| #[inline] // 编译器自己决定是否内联
#[inline(always)] // 强制内联
#[inline(never)] // 强制不内联
fn hot_function() {
#[inline]
let x = 1 + 2;
}
|
12.3.4.2 #[cold](提示编译器该路径不常执行)
#[cold] 提示编译器这条路径很少执行,便于优化热路径的代码布局,不影响语义。
1
2
3
4
5
| #[cold]
fn error_handler() {
// 这个函数不常被调用
println!("处理错误...");
}
|
12.3.4.3 #[track_caller](传递调用位置信息)
#[track_caller] 让函数里取到的位置指向调用者,panic!/unwrap 的报错位置就靠它。
1
2
3
4
5
6
7
8
9
10
11
12
| fn inner() {
println!("被调用的位置: {:?}", std::panic::Location::caller());
}
#[track_caller]
fn outer() {
inner(); // 会打印 outer 的调用位置
}
fn main() {
outer();
}
|
12.3.4.4 #[allow(…)] / #[warn(…)] / #[deny(…)](lint 控制)
allow/warn/deny 用来逐项调整 lint 级别,deny 会把警告升级成编译错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #[allow(unused_variables)]
fn maybe_unused() {
let x = 1; // 不会警告了
}
#[warn(dead_code)]
fn unused_function() {
// 会有警告
}
#[deny(improper_ctypes)]
extern "C" {
fn maybe_bad();
}
|
12.3.4.5 #[deprecated] / #[must_use](未使用时产生警告,应用于函数返回类型)
#[deprecated] 让调用方使用该 API 时收到警告,#[must_use] 则在返回值被丢弃时发出警告。
1
2
3
4
5
6
7
8
9
10
11
12
| #[deprecated(since = "1.0.0", note = "请使用 new_function 代替")]
fn old_function() {}
#[must_use]
fn important_function() -> i32 {
42
}
fn main() {
old_function(); // 警告:old_function 已弃用
important_function(); // 警告:有 must_use 注解但没有使用返回值
}
|
12.3.4.6 #[non_exhaustive](禁止外部 crate 匹配全部变体,强制使用者依赖未来兼容性)
#[non_exhaustive] 禁止外部 crate 穷尽匹配所有变体或字段,为日后的扩展留出余地。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #[non_exhaustive]
pub enum Error {
NotFound,
Unauthorized,
Unknown,
}
fn main() {
let e = Error::NotFound;
match e {
Error::NotFound => { /* ... */ }
// 编译器会强制你添加 _ 分支,因为未来可能添加新的变体
_ => { /* 必须处理其他情况 */ }
}
}
|
12.3.4.7 #[repr(…)](内存布局)
#[repr(...)] 控制内存布局,例如 #[repr(C)] 保证字段顺序与 C 兼容。
1
2
3
4
5
6
7
8
9
| #[repr(C)] // C 风格内存布局
#[repr(Rust)] // Rust 默认布局
#[repr(u8)] // 用 u8 作为枚举的底层类型
#[repr(align(16))] // 对齐到 16 字节
struct CStyle {
a: i32,
b: i64,
}
|
12.3.4.8 #[link(…)](链接外部库)
#[link(...)] 告诉链接器需要链接哪个外部库,通常配合 extern "C" 的声明一起使用。
1
2
3
4
| #[link(name = "ssl")]
extern "C" {
fn SSL_connect(ssl: *mut SSL) -> c_int;
}
|
12.3.4.9 #[no_mangle](禁止编译器改名)
#[no_mangle] 禁止编译器重命名符号,是导出给 C 侧调用的前提。
1
2
3
4
| #[no_mangle]
pub extern "C" fn Rust_exported_function() {
println!("我不会被改名!");
}
|
12.3.4.10 #[export_name](自定义导出符号名)
#[export_name = "..."] 可以指定导出符号的确切名字。
1
2
3
4
| #[export_name = "my_custom_name"]
pub extern "C" fn exported() {
println!("导出符号名是 my_custom_name");
}
|
12.3.4.11 #[derive(…)](标准派生宏)
#[derive(...)] 是最常用的内建属性宏,一次挂上若干自动生成的 trait 实现。
1
2
3
4
5
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
struct Point {
x: i32,
y: i32,
}
|
12.3.4.12 #[cfg(…)](条件编译)
#[cfg(...)] 做条件编译,条件不成立时代码根本不会进入编译。
1
2
3
4
| #[cfg(target_os = "windows")]
fn windows_only() {
println!("只在 Windows 上编译");
}
|
12.3.4.13 #[global_allocator](自定义全局分配器)
#[global_allocator] 用于替换全局分配器,属于 unsafe 领域,一个程序里只能有一个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| use std::alloc::{GlobalAlloc, System, Layout};
struct MyAllocator;
unsafe impl GlobalAlloc for MyAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
}
#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;
fn main() {
let s = String::from("Hello");
println!("{}", s); // 使用自定义分配器
}
|
12.3.5 #[macro_export]
12.3.5.1 宏导出到 crate 根(crate 级别 re-export)
#[macro_export] 把 macro_rules! 宏导出到 crate 根,其他 crate 才能使用它。
1
2
3
4
5
6
7
8
9
| // lib.rs
#[macro_export]
macro_rules! hello {
() => {
println!("Hello!");
};
}
// 这使得外部可以用 crate::hello!() 调用
|
12.3.5.2 跨 crate 宏调用(use crate::macro_name!)
跨 crate 调用宏要求宏已被导出,调用方还要通过 crate::宏名! 或路径把它引入。
// ⚠️ 这段代码属于「库 crate 的调用方」,需要先把上面的宏导出到
// 一个名为 my_crate 的 crate 里才能编译(本块标记为 ignore)。
// 其他 crate 使用
use my_crate::hello;
fn main() {
hello!(); // Hello!
}
12.4 syn / quote / proc-macro2
12.4.1 syn 解析 Rust 代码为 AST
parse_macro_input! 把 TokenStream 解析成 syn 的类型,解析失败会直接变成编译错误。
1
2
3
| use syn::parse_macro_input;
let input = parse_macro_input!(input as DeriveInput);
|
DeriveInput 里包含标识符、泛型和 Data;按 Data::Struct / Data::Enum 分支即可分别处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| use syn::{DeriveInput, Data, Fields};
fn parse_struct(data: &Data) -> String {
match data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(named) => {
let fields: Vec<_> = named.named.iter().map(|f| {
f.ident.as_ref().unwrap().to_string()
}).collect();
format!("结构体字段: {:?}", fields)
}
Fields::Unnamed(unnamed) => {
format!("元组结构体,有 {} 个字段", unnamed.unnamed.len())
}
Fields::Unit => "单元结构体".to_string()
}
}
Data::Enum(e) => format!("枚举有 {} 个变体", e.variants.len()),
Data::Union(u) => "联合体".to_string(),
}
}
|
12.4.1.3 Item / Expr / Pat / Type 解析
syn 为每种语法结构都准备了对应类型:Item 表示项、Expr 表达式、Pat 模式、Type 类型。
1
2
3
4
5
6
7
8
9
10
11
| use syn::{Item, Expr, Pat, Type};
fn handle_item(item: &Item) {
match item {
Item::Fn(func) => println!("函数: {}", func.sig.ident),
Item::Struct(s) => println!("结构体: {}", s.ident),
Item::Enum(e) => println!("枚举: {}", e.ident),
Item::Mod(m) => println!("模块: {}", m.ident),
_ => println!("其他类型的条目"),
}
}
|
12.4.2 quote 将 AST 转换回 Rust 代码
12.4.2.1 quote::quote!(生成 token)
quote! 把写下的代码转成 TokenStream,是过程宏里生成代码的主力工具。
1
2
3
4
5
6
7
| use quote::quote;
let tokens = quote! {
fn hello() {
println!("Hello!");
}
};
|
12.4.2.2 #var(变量替换)
在 quote! 里写 #var,会把变量插入到生成代码的对应位置。
1
2
3
4
5
6
7
| let name = "World";
let expanded = quote! {
fn greet() {
println!("Hello, #name!");
}
};
// 展开后:fn greet() { println!("Hello, World!"); }
|
12.4.2.3 #(#var)*(迭代展开)
#(#var)* 对可迭代的值做重复展开,常用于按字段列表批量生成代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| let fields = vec!["x", "y", "z"];
let expanded = quote! {
struct Point {
#(
#fields: i32,
)*
}
};
// 展开后:
// struct Point {
// x: i32,
// y: i32,
// z: i32,
// }
|
12.4.2.4 可选展开:直接插值 Option
quote! 没有 #var? 这种语法。需要「有就展开、没有就省略」时,直接插入 Option<T> 即可:quote 为 Option<T: ToTokens> 实现了 ToTokens,None 会输出空 token。
1
2
3
4
5
6
7
8
| // 已用 quote 1.0 实测:Some 输出 ": Clone",None 输出空
let maybe_bound: Option<TokenStream> = Some(quote! { : Clone });
let expanded = quote! {
fn foo #maybe_bound() {}
// Some → fn foo : Clone () {}
// None → fn foo () {}
};
|
12.4.3 proc-macro2 跨平台支持
12.4.3.1 proc-macro2 的作用(解析后的 token 跨平台表示)
proc-macro2 提供了一份脱离编译器进程也能使用的 TokenStream,让宏逻辑更容易写单元测试。
1
2
3
4
5
6
7
| use proc_macro2::TokenStream;
fn process(tokens: TokenStream) -> TokenStream {
// proc-macro2 的 TokenStream 在不同平台上有相同的行为
// 比 proc_macro::TokenStream 更可靠
tokens
}
|
12.4.4 编写完整的派生宏
12.4.4.1 proc-macro crate 项目结构
过程宏 crate 要在 Cargo.toml 里写 proc-macro = true,并且单独放在一个 crate 中。
1
2
3
4
| my-derive-macro/
├── Cargo.toml
├── src/
│ └── lib.rs
|
1
2
3
4
5
6
7
8
| # Cargo.toml
[lib]
proc-macro = true # 标记为过程宏 crate
[dependencies]
syn = { version = "2.0", features = ["full", "derive"] }
quote = "1.0"
proc-macro2 = "1.0"
|
派生宏的入口先解析 DeriveInput,再根据字段信息决定生成什么。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data};
#[proc_macro_derive(Hello)]
pub fn hello_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let name = &ident;
// data 包含结构体/枚举/联合体的信息
let _ = match &data {
Data::Struct(_) => "结构体",
Data::Enum(_) => "枚举",
Data::Union(_) => "联合体",
};
let expanded = quote! {
impl #name {
pub fn hello() {
println!("Hello from #name!");
}
}
};
expanded.into()
}
|
12.4.4.3 生成代码
最后把生成的 TokenStream 转成 proc_macro::TokenStream 返回,编译器就会用这段代码替换原来的派生属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| #[proc_macro_derive(Getters)]
pub fn derive_getters(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
let getters = match &data {
Data::Struct(data) => {
data.fields.iter().filter_map(|f| {
let name = f.ident.as_ref()?;
let ty = &f.ty;
Some(quote! {
pub fn #name(&self) -> &#ty {
&self.#name
}
})
}).collect::<Vec<_>>()
}
_ => vec![]
};
let expanded = quote! {
impl #ident {
#(#getters)*
}
};
expanded.into()
}
|
12.4.4.4 测试派生宏(trybuild)
派生宏的「编译失败场景」通常交给 trybuild crate 测试:把期望报错的源码放在 tests/ui/ 下,用 TestCases 比对编译器输出(第一次运行会生成 .stderr 基准文件,之后便可逐字比对)。
1
2
3
| # Cargo.toml
[dev-dependencies]
trybuild = "1.0"
|
1
2
3
4
5
6
7
| // tests/compile_tests.rs
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/pass/*.rs"); // 这些文件应当编译通过
t.compile_fail("tests/ui/fail/*.rs"); // 这些文件应当编译失败
}
|
注意这个 crate 就叫 trybuild,并不存在名为 trybuild2 的版本。
12.5 内联宏与调试
12.5.1 编译期宏
12.5.1.1 stringify!(…)(转字符串)
stringify! 在编译期把一段代码原样变成字符串字面量。
1
2
3
4
| fn main() {
let code = stringify!(let x = 1 + 2;);
println!("{}", code); // let x = 1 + 2;
}
|
12.5.1.2 std::hint::black_box(b: T)(阻止编译器优化,消除死代码;用于性能测试)
black_box 阻止编译器把输入当常量折叠、或把结果当死代码删掉,写基准测试时几乎必用。
1
2
3
4
5
6
7
8
9
10
11
| use std::hint::black_box;
fn expensive_computation() -> i32 {
let result = (0..1000).sum();
result
}
fn main() {
let x = expensive_computation();
black_box(x); // 告诉编译器"可能使用了 x",防止优化掉
}
|
12.5.1.3 std::hint::unreachable_unchecked()(声明某条路径不可达)
要告诉编译器「这条路径永远走不到」,标准库提供两个工具:
unreachable!() 宏:安全,万一真的执行到会 panic。std::hint::unreachable_unchecked() 函数:需要 unsafe,万一真的执行到就是未定义行为(UB)。
下面这段 if / else if / else 已经覆盖了全部整数情况,但编译器不会替我们「证明」这一点,所以 else 分支不能直接省掉返回值:
1
2
3
4
5
6
7
8
9
10
11
12
| fn categorize(n: i32) -> &'static str {
if n < 0 {
"negative"
} else if n == 0 {
"zero"
} else if n > 0 {
"positive"
} else {
// 只有 unsafe 才能宣称「不可能到这里」;返回类型是 !,可以强制转换成 &str
unsafe { std::hint::unreachable_unchecked() }
}
}
|
实际工程里更推荐改写成 match,让编译器自己确认穷尽,完全不需要 UB:
1
2
3
4
5
6
7
| fn categorize_safe(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
_ => "positive",
}
}
|
12.5.1.4 concat!(“a”, “b”)(编译期拼接)
concat! 在编译期把多个字面量拼成一个字符串,不产生任何运行时开销。
1
2
3
4
| fn main() {
let s = concat!("Hello", ", ", "World", "!");
println!("{}", s); // Hello, World!
}
|
12.5.1.5 env!(“VAR_NAME”)(读取环境变量,编译期求值(值运行时存在),不存在则编译错误)
env! 在编译期读取环境变量,读不到就直接编译失败;CARGO_PKG_* 由 Cargo 注入。
// ⚠️ CARGO_PKG_VERSION / CARGO_PKG_NAME 由 cargo 在编译期注入,
// 直接用 rustc 编译会报「environment variable not defined」。
// 请放进用 cargo 管理的项目里运行(本块标记为 ignore)。
fn main() {
let version = env!("CARGO_PKG_VERSION");
let name = env!("CARGO_PKG_NAME");
println!("{} version {}", name, version);
// 输出:my-project version 0.1.0
}
12.5.1.6 option_env!(“VAR_NAME”)(读取环境变量,返回 Option,不存在为 None)
option_env! 同样在编译期读取,但读不到时返回 None 而不是报错。
1
2
3
4
| fn main() {
let git_commit = option_env!("GIT_COMMIT");
println!("Git commit: {:?}", git_commit); // None 或 Some("abc123...")
}
|
12.5.1.7 file!() / line!() / column!() / module_path!()(源码位置)
这几个宏在编译期展开成源码位置信息,打日志时定位问题非常方便。
1
2
3
4
5
6
| fn main() {
println!("文件: {}", file!()); // 文件: src/main.rs
println!("行号: {}", line!()); // 行号: 3
println!("列号: {}", column!()); // 列号: 22
println!("模块路径: {}", module_path!()); // 模块路径: test_crate
}
|
12.5.2 宏的调试技巧
12.5.2.1 cargo expand(展开宏查看结果)
cargo-expand 把宏展开后的代码打印出来,是排查宏问题的第一工具。
1
2
3
4
5
6
7
8
| # 安装
cargo install cargo-expand
# 使用
cargo expand # 展开所有宏
cargo expand hello # 展开 hello 模块中的宏
cargo expand --lib # 只展开 lib 中的宏
cargo expand main # 只展开 main 函数中的宏
|
12.5.2.2 编译错误追踪
当宏展开出错时,看编译器的错误信息。错误会显示在展开后的代码上,不是在宏定义处:
macro_rules! bad_macro {
($x:expr) => {
let y: i32 = $x; // 如果 $x 不能转为 i32,这里会报错
};
}
fn main() {
bad_macro!("hello"); // 编译错误,展开后类型不匹配
}
12.5.2.3 宏调试日志
在宏里打印「编译期能看到的信息」,是排查「为什么没匹配上」的常用手段。
1
2
3
4
5
| macro_rules! debug_macro {
( $($x:expr),* ) => {
$( println!("{:?}", stringify!($x)); )*
};
}
|
12.5.3 inline const 表达式(Rust 2024)
12.5.3.1 const { … } 语法(编译期求值块,Rust 1.79+)
const { ... } 块(Rust 1.79+)强制在编译期求值,可以把常量计算直接写在使用处。
1
2
3
4
5
6
7
8
9
| fn main() {
// 在编译期求值 const 块
const VALUE: i32 = const { 1 + 2 + 3 };
println!("VALUE = {}", VALUE); // VALUE = 6
// 可以在任何需要 const 的地方使用
static ARR: [i32; const { 10 * 2 }] = [0; const { 10 * 2 }];
println!("{:?}", ARR); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
|
本章小结
这一章我们全面学习了 Rust 的宏系统:
- 宏 vs 函数:宏在编译期展开,函数在运行时调用;宏可以生成语法,函数不能
- 声明宏 macro_rules!:基于模式匹配的编译期代码生成
- 重复模式:$(…)*(零次或多次)、$(…)+(一次或多次)、$(…)?(零次或一次)
- 片段捕获:expr、stmt、ty、pat、ident、block、meta、tt、path、literal
- Hygiene:卫生宏不会污染外部作用域
- 过程宏:TokenStream → TokenStream 的转换函数
- #[derive(…)]:派生宏自动实现 trait
- 属性宏:给条目附加额外行为
- syn / quote / proc-macro2:过程宏开发三剑客
- 内置属性:inline、cold、track_caller、allow/deprecated/must_use 等
- 内联宏:stringify!、env!、option_env!、file!、line! 等
- cargo expand:调试宏的利器
记住:宏是 Rust 里最强大的特性之一,但也可能是最容易出错的特性。用宏的时候,多用 cargo expand 来看看展开后的代码是不是你想要的。写宏一时爽,调试火葬场——这不是吓你,是真的。
“在 Rust 的世界里,宏就是你的代码生成器。学会用它,你就拥有了’复制粘贴’的超级能力——一次编写,无数次自动生成!”
温馨提醒:过程宏的完整项目实践(比如写一个 #[derive(Getters)])需要你创建一个单独的 crate 并且设置 proc-macro = true。纸上得来终觉浅,绝知此事要躬行——快去动手试试吧!