- Macro和函数的区别可以参考官方文档. 简单地说Macro是Rust元编程的强大武器, 和C++的模板在同一个逻辑层面. 和C语言的宏最大的区别不是C语言宏的简单文本替换, 而是语法层面的处理.
- Marcos的扩展时机是在类型检查之前, 也远在机器代码码生成前. 宏会被特换成Rust代码.
- Rust Macro的同样也是之前说过的基于模式匹配, 因此不会出现插入不对称括号的Rust代码等这类错误.
- Rust在扩展Macro时必须要先定义, 这和函数不同, 函数的定义位置顺序没有要求, 而Rust代码在编译时遇到Macro必须先展开, 而不会继续走下去.
Macro模式匹配规则
以
assert_eq!
为例, 来解释下Macro的模式匹配:assert_eq
是Macro的名称, 后面紧随的delimiter没有强制要求是大括号, 甚至可以是小括号或者中括号. 调用时也是一样, 下面的都是合法的:1
2
3
4
5
6assert_eq!(gcd(6, 10), 2);
assert_eq![gcd(6, 10), 2];
assert_eq!{gcd(6, 10), 2};
let x = vec![1,2,3];
let y = vec!{1,2,3};⇒
符号左边的叫MacroMatcher
, 简单地说可以理解为pattern, 即模式匹配. 它的匹配规则是以token
为导向的, 这点和正则匹配不同(以字符为导向的匹配). 因此在pattern里加入空格或者注释都是允许的.每个pattern由一对
$name:designator
(代号)组成, 前者是代号的变量名, 后者代号指示这个变量将按哪种类型规则来匹配.常见的有:
item
: an item, such as a function, a struct, a module, etc.block
: a block (i.e. a block of statements and/or an expression, surrounded by braces)stmt
: a statementpat
: a patternexpr
: an expressionty
: a typeident
: an identifierpath
: a path (e.g.foo
,::std::mem::replace
,transmute::<_, int>
, …)meta
: a meta item; the things that go inside#[...]
and#![...]
attributestt
: a single token tree
比较难理解的是多重匹配, 格式如下:
中间的部分和单个pattern一样, 不同的是多了一对
$()
和,*
,*
号代表前面$(...)
内的内容会出现0或多次, 和正则一样. 而逗号,
只是一个分隔符, 甚至不要求是逗号.编译(展开)宏的时候, 重复pattern的内容用法也和上面基本类似, 多的也只是一个
$(...)*
符号, 用下面的比较好理解:对于每一对出现对
$key
和$value
, 都将用$(...)*
内的hm.insert(...);
语句替换.
Macro调试
有的时候不知道自己写的macro最终被扩展成什么样子, 这时候有两种方法来打印出rustc
扩展后的代码:
直接调用
rustc
编译器(需要nightly版本):1
cargo +nightly rustc --profile=check -- -Zunstable-options --pretty=expanded
用第三方库cargo-expand , 它有个优点就是打印出来的是格式化且带语法高亮的代码, 可读性非常好.
1
2
3
4
5
6
7
8
9
10cargo expand
or expand a particular test target:
cargo expand --test test_something
or expand bench target
cargo expand --bench bench_name
or expand a specific module or type or function only:
cargo expand path::to::module
最后, 发现一篇对macro解释非常好的文章, 可以参考:
A Beginner’s Guide to Rust Macros ✨