到目前為止你已經學到了不少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.學院派研究
- 勘誤