宏命令

2018-08-12 22:03 更新

宏命令

現在你已經了解了很多 Rust 為抽象和重用代碼提供的工具。這些代碼重用的單元有豐富的語義結構。例如,函數有一個類型聲明,有特征約束的類型參數,重載的函數必須屬于一個特定的特征?!   ?/p>

這種結構意味著 Rust 的核心抽象有強大的編譯時正確性檢查。但這是以靈活性的減少為代價。如果你從表面上識別重復代碼的模式,你可能會發(fā)現像一個泛型函數,特征,或者 Rust 語義中其它任何東西一樣表達模式是很困難的或者是很繁瑣的。

宏定義允許我們實現語法水平上的抽象。宏調用的簡單來說就是“擴大”語法形式。這種擴張發(fā)生在編譯早期,在任何靜態(tài)檢查之前。因此,宏可以捕獲許多代碼重用模式,這些是 Rust 的核心抽象做不到的?!   ?/p>

缺點是基于宏的代碼比較難以理解,因為更少的內置規(guī)則可以使用。像一個普通的函數,可以使用一個功能良好的宏而無需理解它的實現。然而,很難設計一個功能良好的宏!此外,在宏代碼中的編譯錯誤難以解釋,因為他們用擴展代碼來描述問題,而不是開發(fā)人員使用的源代碼級別的形式。

這些缺點是宏的重要的“特性”。這并不是說宏不好,它是有時是 Rust 的一部分,因為他們真正需要簡潔、抽象的代碼。我們要記住這個折衷。

定義一個宏

你可能看到過宏 vec!,用于初始化包含任意數量元素的向量。

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
};

我們可以使用一個宏實現這個函數:

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!。感嘆號是調用語法的一部分,用來區(qū)分一個宏和一個普通的函數。

匹配

宏是通過一系列的規(guī)則來定義的,這些規(guī)則是用來模式匹配的。上面,我們有

( $( $x:expr ),* ) => { ... };

這就像一個匹配表達式的處理器,但編譯時匹配發(fā)生 Rust 語法樹。在最后的實例后面分號是可選的。= > 左邊”的“模式”被稱為“匹配器”。這些在語言里都有自己的小語法。    

匹配器 $x:expr 通過將語法樹綁定到元變量 $x 來匹配任何 Rust 表達式。標識符 expr 是一個片段說明符,完整的標識符是在本章后面的枚舉。$(...),* 周圍的匹配器將匹配零個或多個表達式,這些表達式由逗號分隔開?!   ?/p>

除了特殊的匹配器語法,任何出現在一個匹配器中的 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`

擴展

在大多數情況下,宏觀規(guī)則的右邊是普通的 Rust 語法。但我們可以拼接一些被匹配器捕獲的語法。下面是一個典型的例子:

$(
temp_vec.push($x);
)*

每個匹配表達式 $x 在宏擴展中產生一個單獨的 push 語句。擴展的副本與匹配器中的副本是同步的。    

因為 $x 已經聲明為一個表達式匹配,我們不要在右側重復 :expr。同樣,我們不能把逗號作為重復操作符的一部分。相反,我們在重復的塊內有一個終止分號?!   ?/p>

另一個細節(jié):vec! 宏在右側有兩個雙括號。他們通常組合如下:

macro_rules! foo {
() => {{
...
}}
}

外層的括號是語法 macro_rules! 的一部分。實際上,你也可以使用 () 或 []。他們只是將右側劃分為一個整體。    

內層括號是擴展語法的一部分。記住,vec! 宏被用在一個表達式上下文。為了寫一個包含多個語句的表達式,包括 let-bindings,我們需要使用一個塊。如果你的宏擴展到一個單個表達式,你就不需要這些額外的括號?!   ?/p>

注意,我們從未聲明宏產生一個表達式。事實上,這是不確定的,直到我們作為一個表達式使用宏。小心,你可以編寫一個宏,它的擴展可以在多個上下文中起作用。例如,數據類型的簡寫作為一個表達式或模式是有效的。

重復

重復操作符遵循以下兩個主要規(guī)則:

  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]);
}

上面包含了大部分的匹配器語法。這些例子使用 $(...)* ,這是一種“零個或多個”匹配?;蛘吣憧梢詫?$(...)+ 進行“一個或多個”匹配。兩種形式都可選地包括一個分隔符,它可以是任何除 +* 的符號?!   ?/p>

該系統(tǒng)是基于“Macro-by-Example”的。

衛(wèi)生

一些語言通過使用簡單的文本替換來實現宏,從而導致各種各樣的問題。例如,這個 C 程序打印 13,而不是預期的 25。

#define FIVE_TIMES(x) 5 * x

int main() {
printf("%d\n", FIVE_TIMES(2 + 3));
return 0;
}

擴展后,我們有 5 * 2 + 3,乘法有比加法更高的優(yōu)先級。如果你使用很多 C 宏,你可能知道以避免這個問題的通用方法,還有其它五六種方法。在 Rust 里,我們不必擔心這些。

macro_rules! five_times {
($x:expr) => (5 * $x);
}

fn main() {
assert_eq!(25, five_times!(2 + 3));
}

元變量 $x 被解析為一個表達式節(jié)點,并保持它在語法樹上的位置,即使在替換以后?!   ?/p>

宏觀系統(tǒng)的另一個常見的問題是“變量捕獲”。這里有一個 C 宏,使用 a GNU C extension模擬 Rust 的表達式塊。

#define LOG(msg) ({ \
int state = get_log_state(); \
if (state > 0) { \
printf("log(%d): %s\n", state, msg); \
} \
})

這是一個簡單發(fā)生嚴重故障的用例:

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 第二個變量覆蓋了第一個變量。這是一個問題,因為 print 語句應該參考這兩個變量?!   ?/p>

下面 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 有一個衛(wèi)生宏系統(tǒng)。每個宏擴展發(fā)生在一個獨特的“語法語境”,每個變量被產生它的語法語境所標記。好像 main 里面的變量 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 綁定和循環(huán)標簽,而不適用于 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 提供了兩種語法擴展以幫助宏調試?,F在,他們是不穩(wěn)定的?!   ?/p>

  1. log_syntax!(...) 將其參數打印到標準輸出,在編譯時,沒有“擴展”?!  ?/li>
  2. trace_macros!(true) 在每次宏擴展時產生編譯器信息。在擴展結束時使用 trace_macros!(false) 。

語法要求

即使當 Rust 代碼包含 un-expanded 宏時,它可以解析為一個完整的語法樹。這個屬性對編輯器和其他處理代碼的工具是非常有用的。它也對 Rust 宏系統(tǒng)的設計產生一些后果?!   ?/p>

一個后果是,解析一個宏調用時 Rust 必須確定宏是否代表

  • 零個或多個項目,
  • 零個或多個方法,   
  • 一個表達式,
  • 一個語句,或  
  • 一個模式。

在代碼塊中的宏調用可以代表一些項目,或者一個表達式或語句。Rust 使用一個簡單的規(guī)則來消除這個不確定性。宏調用的代表項目必須是

  • 由花括號分隔開,例如 foo! { ... } ,或者
  • 由一個分號終止,例如 foo!(...);

擴展前解析另一個后果是宏調用必須由有效 Rust 符號組成。此外,圓括號,方括號,花括號必須在一個宏調用中是平衡的。例如,foo!([)是禁止的。這允許 Rust 知道宏調用在哪里結束。    

更正式地,宏調用體必須是一個“標記樹”序列。一個標記樹遞歸地定義為

  • 一系列匹配的由 () ,[] ,或 {} 包圍的標記樹,或者
  • 任何其它單個標記

在一個匹配器中,每個元變量都有一個“片段說明符”,來識別匹配哪些語法形式。

  • ident:標識符。例如:x; foo
  • path:一個合格的名字。例如: T::SpecialA。
  • expr: 一個表達式。例如:2 + 2; if true then { 1 } else { 2 }; f(42)。
  • ty:一個類型。例如:i32; Vec<(char, String)>; &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:一個單個標記樹。

還有其他關于元變量后下一個標記的附加規(guī)則:

  • 變量 expr 后必須加下面中的一個: => , ;
  • 變量 ty 和 path 后必須加下面中的一個: => , : = > as
  • 變量 pat 后必須加下面中的一個:=> , =
  • 其它變量后可能要加其它符號。

這些規(guī)則提供一些在不破壞現有宏的情況下,Rust 語法發(fā)展的靈活性。

宏系統(tǒng)不處理解析的不明確性。例如,語法 $($t:ty)* $e:expr 總是無法解析,因為解析器將被迫選擇解析 $t 和解析 $e。將調用語法改為在前面加一個獨特的符號可以解決這個問題。在這種情況下,你可以編寫 $(T $t:ty)* E $e:exp

范圍和宏導入/導出

宏在編譯的早期階段名稱解析前被擴展。一個缺點是,相對于其他結構的語言,范圍工作原理不同。    

宏的定義和擴展都發(fā)生在一個 crate 資源的深度優(yōu)先,詞序遍歷。所以一個定義在模塊范圍內的宏對在同一個模塊內任何后續(xù)代碼是可見的,其中包括任何后續(xù)的孩子 mod 項目的主體。

一個定義在單個 fn 的體內的宏,或其它不在模塊范圍的任何地方,只有在這個項目內是可見的?!   ?/p>

如果一個模塊有 macro_use 屬性,其宏在它的孩子模塊的 mod 項目后的父模塊中是可見的。如果父模塊也有 macro_use 屬性,那么宏在父模塊的 mod 項目后的祖父模塊中也是可見的,等等。    

macro_use屬性也可以出現在 extern crate。在這種情況下它控制從 extern crate 加載哪些宏,如

#[macro_use(foo, bar)]
extern crate baz;

如果屬性被簡單定義如 #[macro_use],所有宏被加載。如果沒有 #[macro_use] 那么 宏就不能被加載。只有定義 #[macro_export] 屬性的宏可能被加載。    

為了加載沒有連接到輸出的 crate 的宏,使用 #[no_link]?!   ?/p>

一個例子:

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 將被導入?!   ?/p>

Rust 參考有一個宏相關屬性的列表。

變量 $crate

進一步困難發(fā)生在當一個宏在多個 crates 被使用。也就是 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 將失去作用 ?!   ?/p>

Rust 沒有針對 crate 參考的衛(wèi)生系統(tǒng),但它確實提供了一個解決這個問題的簡單方法。在一個從一個名為foo的 crate 引入的宏,特殊宏變量 $crate 將擴展到 ::foo。相反,當一個宏被定義,然后在同一 crate 中被使用,$crate 就不會擴展。這意味著我們可以這樣寫

#[macro_export]
macro_rules! inc {
($x:expr) => ( $crate::increment($x) )
}

來定義一個在庫內庫外的宏。函數名也擴展到 ::increment 或者 ::mylib::increment。

為了保持這個系統(tǒng)簡單而正確,#[macro_use] extern crate ... 可能只出現在 crate 的根部,而不是 mod 內部。這將確保 $crate 是一個標識符。

深端

介紹性章節(jié)曾今提到過遞歸宏,但它沒有給出完整的描述。遞歸宏是有用的另一個原因:每個遞歸調用給你匹配宏參數的另一個機會?!   ?/p>

作為一個極端的例子,盡管不明智,在 Rust 的宏系統(tǒng)實現位循環(huán)標記自動機是可能的。

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! 宏的定義中的復制。

常見的宏

下面是一些你會在 Rust 代碼中常見的宏。

panic!

這個宏會導致當前線程的叛逆。你可以給它一個消息產生叛逆:

panic!("oh no!");

vec!

vec !宏在整本書被使用,所以你可能已經見過了。它毫不費力地創(chuàng)建 Vec:

let v = vec![1, 2, 3, 4, 5];

它還允許你用重復的值構建向量。例如,一百個 0:

let v = vec![0; 100];

assert! 和 assert_eq!

這兩個宏用于測試。assert! 傳入一個布爾值,assert_eq! 傳入兩個值并且比較它們。像下面這樣:

// A-ok!

assert!(true);
assert_eq!(5, 3 + 2);

// nope :(

assert!(5 < 3);
assert_eq!(5, 3);

try!

try! 用于錯誤處理。它傳入可以返回 Result<T, E> 的參數,并給出 T 如果它是 Ok,并且返回 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!

當你認為一些代碼不應該執(zhí)行是使用這個宏:

if false {
unreachable!();
}

有時,編譯器可能會讓你有一個不同的分支,你知道它永遠不會運行。在這些情況下,使用這個宏,這樣如果你錯了,你會得到一個關于它的 panic!。

let x: Option<i32> = None;

match x {
Some(_) => unreachable!(),
None => println!("I know x is None!"),
}

unimplemented!

當你想讓你的函數檢查類型,不想擔心寫出函數的主體時 unimplemented! 宏可以被使用。這種情況的一個例子是你想要一次同時用多個所需的方法實現一個特征。把其它的定義為 unimplemented! 直到你準備寫他們。

程序宏

如果 Rust 宏系統(tǒng)做不到你需要的,你可能想要編寫一個編譯器插件。相比 macro_rules !宏,這需要更多的工作,不太穩(wěn)定的接口,bugs 可能更難追蹤。作為交換你獲得了在編譯器內運行任意 Rust 代碼的靈活性。這就是語法擴展插件有時被稱為“程序宏”的原因。

在效率和可重用性方面, vec! 在 libcollections 的實際定義不同于這里介紹的。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號