到目前为止你已经学到了不少Rust提供的抽象和重用代码的工具了。这些代码重用单元有丰富的语义结构。例如,函数有类型标记,类型参数有特性限制并且能重载的函数必须属于一个特定的特性。
这些结构意味着Rust核心抽象拥有强大的编译时正确性检查。不过作为代价的是灵活性的减少。如果你识别出一个重复代码的模式,你会发现把它们解释为泛型函数,特性或者任何Rust语义中的其它结构很难或者很麻烦。
宏允许我们在_句法_水平上进行抽象。宏是一个“可扩展”句法形式的速记。这个扩展发生在编译的早期,在任何静态检查之前。因此,宏可以实现很多Rust核心抽象不能做到的代码重用模式。
缺点是基于宏的代码更难懂,因为它很少利用Rust的内建规则。就像一个常规函数,一个通用的宏可以在不知道其实现的情况下使用。然而,设计一个通用的宏困难的!另外,在宏中的编译错误更难解释,因为它在扩展代码上描述问题,恶如不是在开发者使用的代码级别。
这些缺点让宏成了所谓“最后求助于的功能”。这并不是说宏的坏话;只是因为它是Rust中需要真正简明,良好抽象的代码的部分。切记权衡取舍。
## 定义一个宏
你可能见过`vec!`宏。用来初始化一个任意数量元素的[vector](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.17.Vectors.html)。
~~~
let x: Vec<u32> = vec![1, 2, 3];
~~~
这不可能是一个常规函数,因为它可以接受任何数量的参数。不过我们可以想象的到它是这些代码的句法简写:
~~~
let x: Vec<u32> = {
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
};
~~~
我们可以使用宏来实现这么一个简写:[1](http://kaisery.gitbooks.io/rust-book-chinese/content/content/5.35.Macros%20%E5%AE%8F.html#1)
~~~
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
~~~
哇哦,这里有好多新语法!让我们分开来看。
~~~
macro_rules! vec { ... }
~~~
这里我们定义了一个叫做`vec`的宏,跟用`fn vec`定义一个`vec`函数很相似。再罗嗦一句,我们通常写宏的名字时带上一个感叹号,例如`vec!`。感叹号是调用语法的一部分用来区别宏和常规函数。
## 匹配
宏通过一系列_规则_定义,它们是模式匹配的分支。上面我们有:
~~~
( $( $x:expr ),* ) => { ... };
~~~
这就像一个`match`表达式分支,不过匹配发生在编译时Rust的语法树中。最后一个分支(这里只有一个分支)的分号是可选的。`=>`左侧的“模式”叫_匹配器_(_matcher_)。它有[自己的语法](http://doc.rust-lang.org/reference.html#macros)。
`$x:expr`匹配器将会匹配任何Rust表达式,把它的语法树绑定到元变量`$x`上。`expr`标识符是一个_片段分类符_(_fragment specifier_)。在[宏进阶章节](http://doc.rust-lang.org/book/advanced-macros.html)(已被本章合并,坐等官方文档更新)中列举了所有可能的分类符。匹配器写在`$(...)`中,`*`会匹配0个或多个表达式,表达式之间用逗号分隔。
除了特殊的匹配器语法,任何出现在匹配器中的Rust标记必须完全相符。例如:
~~~
macro_rules! foo {
(x => $e:expr) => (println!("mode X: {}", $e));
(y => $e:expr) => (println!("mode Y: {}", $e));
}
fn main() {
foo!(y => 3);
}
~~~
将会打印:
~~~
mode Y: 3
~~~
而这个:
~~~
foo!(z => 3);
~~~
我们会得到编译错误:
~~~
error: no rules expected the token `z`
~~~
## 扩展
宏规则的右边是正常的Rust语法,大部分是。不过我们可以拼接一些匹配器中的语法。例如最开始的例子:
~~~
$(
temp_vec.push($x);
)*
~~~
每个匹配的`$x`表达式都会在宏扩展中产生一个单独`push`语句。扩展中的重复与匹配器中的重复“同步”进行(稍后介绍更多)。
因为`$x`已经在表达式匹配中声明了,我们并不在右侧重复`:expr`。另外,我们并不将用来分隔的逗号作为重复操作的一部分。相反,我们在重复块中使用一个结束用的分号。
另一个细节:`vec!`宏的右侧有_两对_大括号。它们经常像这样结合起来:
~~~
macro_rules! foo {
() => {{
...
}}
}
~~~
外层的大括号是`macro_rules!`语法的一部分。事实上,你也可以`()`或者`[]`。它们只是用来界定整个右侧结构的。
内层大括号是扩展语法的一部分。记住,`vec!`在表达式上下文中使用。要写一个包含多个语句,包括`let`绑定,的表达式,我们需要使用块。如果你的宏只扩展一个单独的表达式,你不需要内层的大括号。
注意我们从未_声明_宏产生一个表达式。事实上,直到宏被展开之前我们都无法知道。足够小心的话,你可以编写一个能在多个上下文中扩展的宏。例如,一个数据类型的简写可以作为一个表达式或一个模式。
## 重复(Repetition)
重复运算符遵循两个原则:
1. `$(...)*`对它包含的所有`$name`都执行“一层”重复
2. 每个`$name`必须有至少这么多的`$(...)*`与其相对。如果多了,它将是多余的。
这个巴洛克宏展示了外层重复中多余的变量。
~~~
macro_rules! o_O {
(
$(
$x:expr; [ $( $y:expr ),* ]
);*
) => {
&[ $($( $x + $y ),*),* ]
}
}
fn main() {
let a: &[i32]
= o_O!(10; [1, 2, 3];
20; [4, 5, 6]);
assert_eq!(a, [11, 12, 13, 24, 25, 26]);
}
~~~
这就是匹配器的大部分语法。这些例子使用了`$(...)*`,它指“0次或多次”匹配。另外你可以用`$(...)+`代表“1次或多次”匹配。每种形式都可以包括一个分隔符,分隔符可以使用任何除了`+`和`*`的符号。
这个系统基于[Macro-by-Example](http://www.cs.indiana.edu/ftp/techreports/TR206.pdf)(PDF链接)。
## 卫生(Hygiene)
一些语言使用简单的文本替换来实现宏,它导致了很多问题。例如,这个C程序打印`13`而不是期望的`25`。
~~~
#define FIVE_TIMES(x) 5 * x
int main() {
printf("%d\n", FIVE_TIMES(2 + 3));
return 0;
}
~~~
扩展之后我们得到`5 * 2 + 3`,并且乘法比加法有更高的优先级。如果你经常使用C的宏,你可能知道标准的习惯来避免这个问题,或更多其它的问题。在Rust中,你不需要担心这个问题。
~~~
macro_rules! five_times {
($x:expr) => (5 * $x);
}
fn main() {
assert_eq!(25, five_times!(2 + 3));
}
~~~
元变量`$x`被解析成一个单独的表达式节点,并且在替换后依旧在语法树中保持原值。
宏系统中另一个常见的问题是_变量捕捉_(_variable capture_)。这里有一个C的宏,使用了[GNU C 扩展](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html)来模拟Rust表达式块。
~~~
#define LOG(msg) ({ \
int state = get_log_state(); \
if (state > 0) { \
printf("log(%d): %s\n", state, msg); \
} \
})
~~~
这是一个非常糟糕的用例:
~~~
const char *state = "reticulating splines";
LOG(state)
~~~
它扩展为:
~~~
const char *state = "reticulating splines";
int state = get_log_state();
if (state > 0) {
printf("log(%d): %s\n", state, state);
}
~~~
第二个叫做`state`的参数参数被替换为了第一个。当打印语句需要用到这两个参数时会出现问题。
等价的Rust宏则会有理想的表现:
~~~
macro_rules! log {
($msg:expr) => {{
let state: i32 = get_log_state();
if state > 0 {println!("log({}): {}", state, $msg);
}
}};
}
fn main() {
let state: &str = "reticulating splines";
log!(state);
}
~~~
这之所以能工作时因为Rust有一个[卫生宏系统](http://en.wikipedia.org/wiki/Hygienic_macro)。每个宏扩展都在一个不同的_语法上下文_(_syntax context_)中,并且每个变量在引入的时候都在语法上下文中打了标记。这就好像是`main`中的`state`和宏中的`state`被画成了不同的“颜色”,所以它们不会冲突。
这也限制了宏在被执行时引入新绑定的能力。像这样的代码是不能工作的:
~~~
macro_rules! foo {
() => (let x = 3);
}
fn main() {
foo!();
println!("{}", x);
}
~~~
相反你需要在执行时传递变量的名字,这样它会在语法上下文中被正确标记。
~~~
macro_rules! foo {
($v:ident) => (let $v = 3);
}
fn main() {
foo!(x);
println!("{}", x);
}
~~~
这对`let`绑定和loop标记有效,对[items](http://doc.rust-lang.org/reference.html#items)无效。所以下面的代码可以编译:
~~~
macro_rules! foo {
() => (fn x() { });
}
fn main() {
foo!();
x();
}
~~~
## 递归宏
一个宏扩展中可以包含更多的宏,包括被扩展的宏自身。这种宏对处理树形结构输入时很有用的,正如这这个(简化了的)HTML简写所展示的那样:
~~~
macro_rules! write_html {
($w:expr, ) => (());
($w:expr, $e:tt) => (write!($w, "{}", $e));
($w:expr, $tag:ident [ $($inner:tt)* ] $($rest:tt)*) => {{
write!($w, "<{}>", stringify!($tag));
write_html!($w, $($inner)*);
write!($w, "</{}>", stringify!($tag));
write_html!($w, $($rest)*);
}};
}
fn main() {
use std::fmt::Write;
let mut out = String::new();
write_html!(&mut out,
html[
head[title["Macros guide"]]
body[h1["Macros are the best!"]]
]);
assert_eq!(out,
"<html><head><title>Macros guide</title></head>\
<body><h1>Macros are the best!</h1></body></html>");
}
~~~
## 调试宏代码
运行`rustc --pretty expanded`来查看宏扩展后的结果。输出表现为一个完整的包装箱,所以你可以把它反馈给`rustc`,它会有时会比原版产生更好的错误信息。注意如果在同一作用域中有多个相同名字(不过在不同的语法上下文中)的变量的话`--pretty expanded`的输出可能会有不同的意义。这种情况下`--pretty expanded,hygiene`将会告诉你有关语法上下文的信息。
`rustc`提供两种语法扩展来帮助调试宏。目前为止,它们是不稳定的并且需要功能入口(feature gates)。
* `log_syntax!(...)`会打印它的参数到标准输出,在编译时,并且不“扩展”任何东西。
* `trace_macros!(true)`每当一个宏被扩展时会启用一个编译器信息。在扩展后使用`trace_macros!(false)`来关闭它。
## 句法要求
即使Rust代码中含有未扩展的宏,它也可以被解析为一个完整的[语法树](http://kaisery.gitbooks.io/rust-book-chinese/content/content/7.Glossary%20%E8%AF%8D%E6%B1%87%E8%A1%A8.md#abstract-syntax-tree)。这个属性对于编辑器或其它处理代码的工具来说十分有用。这里也有一些关于Rust宏系统设计的推论。
一个推论是Rust必须确定,当它解析一个宏扩展时,宏是否代替了
* 0个或多个项
* 0个或多个方法
* 一个表达式
* 一个语句
* 一个模式
一个块中的宏扩展代表一些项,或者一个表达式/语句。Rust使用一个简单的规则来解决这些二义性。一个代表项的宏扩展必须是
* 用大括号界定的,例如`foo! { ... }`
* 分号结尾的,例如`foo!(...);`
另一个展开前解析的推论是宏扩展必须包含有效的Rust记号。更进一步,括号,中括号,大括号在宏扩展中必须是封闭的。例如,`foo!([)`是不允许的。这让Rust知道宏何时结束。
更正式一点,宏扩展体必须是一个_记号树_(_token trees_)的序列。一个记号树是一系列递归的
* 一个由`()`,`[]`或`{}`包围的记号树序列
* 任何其它单个记号
在一个匹配器中,每一个元变量都有一个_片段分类符_(_fragment specifier_),确定它匹配的哪种句法。
* `ident`:一个标识符。例如:`x`,`foo`
* `path`:一个合适的名字。例如:`T::SpecialA`
* `expr`:一个表达式。例如:`2 + 2`;`if true then { 1 } else { 2 }`;`f(42)`
* `ty`:一个类型。例如:`i32`;`Vec`;`&T`
* `pat`:一个模式。例如:`Some(t)`;`(17, 'a')`;`_`
* `stmt`:一个单独语句。例如:`let x = 3`
* `block`:一个大括号界定的语句序列。例如:`{ log(error, "hi"); return 12; }`
* `item`:一个项。例如:`fn foo() { }`,`struct Bar`
* `meta`:一个“元项”,可以在属性中找到。例如:`cfg(target_os = "windows")`
* `tt`:一个单独的记号树
对于一个元变量后面的一个记号有一些额外的规则:
* `expr`变量必须后跟一个`=>`,`,`,`;`
* `ty`和`path`变量必须后跟一个`=>`,`,`,`:`,`=`,`>`,`as`
* `pat`变量必须后跟一个`=>`,`,`,`=`
* 其它变量可以后跟任何记号
这些规则为Rust语法提供了一些灵活性以便将来的扩展不会破坏现有的宏。
宏系统完全不处理解析模糊。例如,`$($t:ty)* $e:expr`语法总是会解析失败,因为解析器会被强制在解析`$t`和解析`$e`之间做出选择。改变扩展在它们之前分别加上一个记号可以解决这个问题。在这个例子中,你可以写成`$(T $t:ty)* E $e:exp`。
## 范围和宏导入/导出
宏在编译的早期阶段被展开,在命名解析之前。这有一个缺点是与语言中其它结构相比,范围对宏的作用不一样。
定义和扩展都发生在同一个深度优先,字典顺序的包装箱的代码遍历中。那么在模块范围内定义的宏对同模块的接下来的代码是可见的,这包括任何接下来的子`mod`项。
一个定义在`fn`函数体内的宏,或者任何其它不在模块范围内的地方,只在它的范围内可见。
如果一个模块有`subsequent`属性,它的宏在子`mod`项之后的父模块也是可见的。如果它的父模块也有`macro_use`属性那么在父`mod`项之后的祖父模块中也是可见的,以此类推。
`macro_use`属性也可以出现在`extern crate`。在这个上下文中它控制那些宏从外部包装箱中装载,例如
~~~
#[macro_use(foo, bar)]
extern crate baz;
~~~
如果属性只是简单的写成`#[macro_use]`,所有的宏都会被装载。如果没有`#[macro_use]`属性那么没有宏被装载。只有被定义为`#[macro_export]`的宏可能被装载。
装载一个包装箱的宏_而不_链接到输出,使用`#[no_link]`。
一个例子:
~~~
macro_rules! m1 { () => (()) }
// visible here: m1
mod foo {
// visible here: m1
#[macro_export]
macro_rules! m2 { () => (()) }
// visible here: m1, m2
}
// visible here: m1
macro_rules! m3 { () => (()) }
// visible here: m1, m3
#[macro_use]
mod bar {
// visible here: m1, m3
macro_rules! m4 { () => (()) }
// visible here: m1, m3, m4
}
// visible here: m1, m3, m4
~~~
当这个库被用`#[macro_use] extern crate`装载时,只有`m2`会被导入。
Rust参考中有一个[宏相关的属性列表](http://doc.rust-lang.org/reference.html#macro-related-attributes)。
## `$crate`变量
当一个宏在多个包装箱中使用时会产生另一个困难。让我们说`mylib`定义了
~~~
pub fn increment(x: u32) -> u32 {
x + 1
}
#[macro_export]
macro_rules! inc_a {
($x:expr) => ( ::increment($x) )
}
#[macro_export]
macro_rules! inc_b {
($x:expr) => ( ::mylib::increment($x) )
}
~~~
`inc_a`只能在`mylib`内工作,同时`inc_b`只能在库外工作。进一步说,如果用户有另一个名字导入`mylib`时`inc_b`将不能工作。
Rust(目前)还没有针对包装箱引用的卫生系统,不过它确实提供了一个解决这个问题的变通方法。当从一个叫`foo`的包装箱总导入宏时,特殊宏变量`$crate`会展开为`::foo`。相反,当这个宏在同一包装箱内定义和使用时,`$crate`将展开为空。这意味着我们可以写
~~~
#[macro_export]
macro_rules! inc {
($x:expr) => ( $crate::increment($x) )
}
~~~
来定义一个可以在库内外都能用的宏。这个函数名字会展开为`::increment`或`::mylib::increment`。
为了保证这个系统简单和正确,`#[macro_use] extern crate ...`应只出现在你包装箱的根中,而不是在`mod`中。这保证了`$crate`扩展为一个单独的标识符。
## 深入(The deep end)
之前的介绍章节提到了递归宏,但并没有给出完整的介绍。还有一个原因令递归宏是有用的:每一次递归都给你匹配宏参数的机会。
作为一个极端的例子,可以,但极端不推荐,用Rust宏系统来实现一个[位循环标记](http://esolangs.org/wiki/Bitwise_Cyclic_Tag)自动机。
~~~
macro_rules! bct {
// cmd 0: d ... => ...
(0, $($ps:tt),* ; $_d:tt)
=> (bct!($($ps),*, 0 ; ));
(0, $($ps:tt),* ; $_d:tt, $($ds:tt),*)
=> (bct!($($ps),*, 0 ; $($ds),*));
// cmd 1p: 1 ... => 1 ... p
(1, $p:tt, $($ps:tt),* ; 1)
=> (bct!($($ps),*, 1, $p ; 1, $p));
(1, $p:tt, $($ps:tt),* ; 1, $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; 1, $($ds),*, $p));
// cmd 1p: 0 ... => 0 ...
(1, $p:tt, $($ps:tt),* ; $($ds:tt),*)
=> (bct!($($ps),*, 1, $p ; $($ds),*));
// halt on empty data string
( $($ps:tt),* ; )
=> (());
}
~~~
练习:使用宏来减少上面`bct!`宏定义中的重复。
## 常用宏(Common macros)
这里有一些你会在Rust代码中看到的常用宏
### `panic!`
这个宏导致当前线程恐慌。你可以传给这个宏一个信息通过:
~~~
panic!("oh no!");
~~~
### `vec!`
`vec!`的应用遍及本书,所以你可能已经见过它了。它方便创建`Vec`:
~~~
let v = vec![1, 2, 3, 4, 5];
~~~
它也让你可以用重复值创建vector。例如,100个`0`:
~~~
let v = vec![0; 100];
~~~
### `assert!`和`assert_eq!`
这两个宏用在测试中。`assert!`获取一个布尔值,而`assert_eq!`获取两个值并比较它们。对了就通过,错了就`panic!`(注:原书是Truth passes, success panic!s,个人认为不对)。像这样:
~~~
// A-ok!
assert!(true);
assert_eq!(5, 3 + 2);
// nope :(
assert!(5 < 3);
assert_eq!(5, 3);
~~~
### `try!`
`try!`用来进行错误处理。它获取一些可以返回`Result`的数据,并返回`T`如果它是`Ok`,或`return`一个`Err(E)`如果出错了。像这样:
~~~
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = try!(File::create("foo.txt"));
Ok(())
}
~~~
它比这么写要更简明:
~~~
use std::fs::File;
fn foo() -> std::io::Result<()> {
let f = File::create("foo.txt");
let f = match f {
Ok(t) => t,
Err(e) => return Err(e),
};
Ok(())
}
~~~
### `unreachable!`
这个宏用于当你认为一些代码不应该被执行的时候:
~~~
if false {
unreachable!();
}
~~~
有时,编译器可能会让你编写一个不同的你认为将永远不会执行的分支。在这个例子中,用这个宏,这样如果你以错误结尾,你会为此得到一个`panic!`。
~~~
let x: Option<i32> = None;
match x {
Some(_) => unreachable!(),
None => println!("I know x is None!"),
}
~~~
### `unimplemented!`
`unimplemented!`宏可以被用来当你尝试去让你的函数通过类型检查,同时你又不想操心去写函数体的时候。一个这种情况的例子是实现一个要求多个方法的特性,而你只想一次搞定一个。用`unimplemented!`定义其它的直到你准备好去写它们了。
## 宏程序(Procedural macros)
如果Rust宏系统不能做你想要的,你可能想要写一个[编译器插件](http://kaisery.gitbooks.io/rust-book-chinese/content/content/6.1.Compiler%20Plugins%20%E7%BC%96%E8%AF%91%E5%99%A8%E6%8F%92%E4%BB%B6.md)。与`macro_rules!`宏相比,它能做更多的事,接口也更不稳定,并且bug将更难以追踪。相反你得到了可以在编译器中运行任意Rust代码的灵活性。为此语法扩展插件有时被称为_宏程序_(_procedural macros_)。
* * *
1. 在libcollections中的`vec!`的实际定义与我们在这展示的有所不同,出于效率和可重用性的考虑。
- 前言
- 1.介绍
- 2.准备
- 2.1.安装Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.学习Rust
- 3.1.猜猜看
- 3.2.哲学家就餐问题
- 3.3.其它语言中的Rust
- 4.高效Rust
- 4.1.栈和堆
- 4.2.测试
- 4.3.条件编译
- 4.4.文档
- 4.5.迭代器
- 4.6.并发
- 4.7.错误处理
- 4.8.外部语言接口
- 4.9.Borrow 和 AsRef
- 4.10.发布途径
- 5.语法和语义
- 5.1.变量绑定
- 5.2.函数
- 5.3.原生类型
- 5.4.注释
- 5.5.If语句
- 5.6.for循环
- 5.7.while循环
- 5.8.所有权
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可变性
- 5.12.结构体
- 5.13.枚举
- 5.14.匹配
- 5.15.模式
- 5.16.方法语法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait对象
- 5.24.闭包
- 5.25.通用函数调用语法
- 5.26.包装箱和模块
- 5.27.`const`和`static`
- 5.28.属性
- 5.29.`type`别名
- 5.30.类型转换
- 5.31.关联类型
- 5.32.不定长类型
- 5.33.运算符和重载
- 5.34.`Deref`强制多态
- 5.35.宏
- 5.36.裸指针
- 6.Rust开发版
- 6.1.编译器插件
- 6.2.内联汇编
- 6.3.不使用标准库
- 6.4.固有功能
- 6.5.语言项
- 6.6.链接参数
- 6.7.基准测试
- 6.8.装箱语法和模式
- 6.9.切片模式
- 6.10.关联常量
- 7.词汇表
- 8.学院派研究
- 勘误