撰寫巨集
撰寫巨集可以到非常複雜。而你幾乎永遠都不需要寫巨集,但有時你可能會因為它們非常方便而想去寫。寫巨集很有趣,因為它們幾乎是不同的語言。寫巨集時你實際上會用到另一個叫 macro_rules!
的巨集。然後加入你的巨集名稱,並開啟 {}
區塊。裡面有點像 match
陳述式。
這裡有個巨集的範例只有接受 ()
,也只回傳 6:
macro_rules! give_six { () => { 6 }; } fn main() { let six = give_six!(); println!("{}", six); }
但它和 match
陳述式不太一樣,因為巨集實際上不會編譯任何東西。它只是接受一個輸入並給出一個輸出。然後編譯器會檢查它是否合理。這就是為什麼巨集就像是"寫程式碼的程式碼"。你會記得,真正的 match
陳述式需要給出相同的型別,所以這個就會不能編譯:
fn main() { // ⚠️ let my_number = 10; match my_number { 10 => println!("You got a ten"), _ => 10, } }
它會抱怨你在一種情況下要回傳 ()
,卻在另一種情況下要回傳 i32
。
error[E0308]: `match` arms have incompatible types
--> src\main.rs:5:14
|
3 | / match my_number {
4 | | 10 => println!("You got a ten"),
| | ------------------------- this is found to be of type `()`
5 | | _ => 10,
| | ^^ expected `()`, found integer
6 | | }
| |_____- `match` arms have incompatible types
但巨集並不關心,因為它只是給予輸出。它不是編譯器——它是程式碼的程式碼。所以你可以這樣做:
macro_rules! six_or_print { (6) => { 6 }; () => { println!("You didn't give me 6."); }; } fn main() { let my_number = six_or_print!(6); six_or_print!(); }
這就好辦了,印出 You didn't give me 6.
。你也可以看到,這不是匹配陳述式的分支,因為沒有 _
的情況。我們只能給它 (6)
,或者 ()
,其他的都會出錯。而我們給它的 6
甚至不是 i32
,只是輸入的 6。其實你可以設定任何東西作為巨集的輸入,因為它只查看輸入見到了什麼。比如說:
macro_rules! might_print { (THis is strange input 하하はは哈哈 but it still works) => { println!("You guessed the secret message!") }; () => { println!("You didn't guess it"); }; } fn main() { might_print!(THis is strange input 하하はは哈哈 but it still works); might_print!(); }
所以這個奇怪的巨集只回應兩件事。()
和 (THis is strange input 하하はは哈哈 but it still works)
。沒有其他的東西。印出:
You guessed the secret message!
You didn't guess it
所以巨集不完全是 Rust 語法。但是巨集也可以理解你給它的不同型別的輸入。拿這個例子來說:
macro_rules! might_print { ($input:expr) => { println!("You gave me: {}", $input); } } fn main() { might_print!(6); }
會印出 You gave me: 6
。$input:expr
的部分很重要。它的意思是"對於表達式,給它取變數名稱為 $input"。巨集中的變數是以 $
開頭。在這個巨集中,如果你給它表達式,表達式就會印出來。讓我們再來多試幾次:
macro_rules! might_print { ($input:expr) => { println!("You gave me: {:?}", $input); // 現在我們將會使用 {:?} 因為我們將會給它不同的種類的表達式 } } fn main() { might_print!(()); // 給它 () might_print!(6); // 給它 6 might_print!(vec![8, 9, 7, 10]); // 給它向量 }
會印出:
You gave me: ()
You gave me: 6
You gave me: [8, 9, 7, 10]
另外注意,我們寫的是 {:?}
,但它不會檢查 &input
是否實現了 Debug
。它只會寫程式碼,並嘗試讓它編譯,如果不行,那它就會給出錯誤。
那麼除了 expr
fragment,巨集還能看到什麼呢?它們是 block | expr | ident | item | lifetime | literal | meta | pat | path | stmt | tt | ty | vis
。這就是複雜的部分。你可以在這裡看到它們各自的意思,這裡說:
item: an Item
block: a BlockExpression
stmt: a Statement without the trailing semicolon (except for item statements that require semicolons)
pat: a Pattern
expr: an Expression
ty: a Type
ident: an IDENTIFIER_OR_KEYWORD
path: a TypePath style path
tt: a TokenTree (a single token or tokens in matching delimiters (), [], or {})
meta: an Attr, the contents of an attribute
lifetime: a LIFETIME_TOKEN
vis: a possibly empty Visibility qualifier
literal: matches -?LiteralExpression
有個好網站叫 cheats.rs,在這裡解釋了它們,並且為每一種 fragment 給出範例。
然而對於大多數巨集,你只會用到 expr
、ident
和 tt
。ident
表示識別字,用於變數或函式名稱。tt
表示標記樹 (Token tree),和任何型別的輸入。讓我們嘗試用前兩者寫個簡單的巨集。
macro_rules! check { ($input1:ident, $input2:expr) => { println!( "Is {:?} equal to {:?}? {:?}", $input1, $input2, $input1 == $input2 ); }; } fn main() { let x = 6; let my_vec = vec![7, 8, 9]; check!(x, 6); check!(my_vec, vec![7, 8, 9]); check!(x, 10); }
所以這將接受一個 ident
(像是變數名)和一個表達式,看看它們是否相同。印出:
Is 6 equal to 6? true
Is [7, 8, 9] equal to [7, 8, 9]? true
Is 6 equal to 10? false
而這裡有一個巨集,它接受輸入 tt
,然後把它印出來。它會先使用叫做 stringify!
的巨集做出字串。
macro_rules! print_anything { ($input:tt) => { let output = stringify!($input); println!("{}", output); }; } fn main() { print_anything!(ththdoetd); print_anything!(87575oehq75onth); }
印出:
ththdoetd
87575oehq75onth
但如果我們給它一些帶有空格、逗號等的東西,它就不會印出來了。它會認為我們給它不止一個元素或額外的資訊,所以它會感到困惑。
這就是巨集開始變得困難的地方。
要一次提供給巨集多個元素,我們必須使用不同的語法。不是原先的 $input
,而是要用 $($input1),*
。這意味著用逗號分隔的零或更多(這就是 *
的意思)元素。如果你想要一個或多個,要改用 +
而不是 *
。
現在我們的巨集看起來像這樣:
macro_rules! print_anything { ($($input1:tt),*) => { let output = stringify!($($input1),*); println!("{}", output); }; } fn main() { print_anything!(ththdoetd, rcofe); print_anything!(); print_anything!(87575oehq75onth, ntohe, 987987o, 097); }
所以它接受任何用逗號隔開的標記樹,並使用 stringify!
把它變成字串,再印出來。印出:
ththdoetd, rcofe
87575oehq75onth, ntohe, 987987o, 097
如果我們使用 +
而不是 *
,它會給出錯誤,因為其中一次呼叫時我們沒有給它輸入。所以 *
是個比較安全一點的選擇。
所以現在我們可以開始見識到巨集的威力了。在接下來的範例中,我們實際上可以做出我們自己的函式:
macro_rules! make_a_function { ($name:ident, $($input:tt),*) => { // 首先你給它函式一個名字, 然後它檢查其它所有東西 fn $name() { let output = stringify!($($input),*); // 它讓其它所有東西變成字串 println!("{}", output); } }; } fn main() { make_a_function!(print_it, 5, 5, 6, I); // 我們想要函式呼叫 print_it() 來印出我們給的其它所有東西 print_it(); make_a_function!(say_its_nice, this, is, really, nice); // 這裡一樣但是我們改了函式名 say_its_nice(); }
印出:
5, 5, 6, I
this, is, really, nice
所以現在我們可以開始瞭解其他的巨集了。你可以見到,我們已經使用的一些巨集相當簡單。這裡是我們過去常用來寫入檔案的 write!
巨集:
#![allow(unused)] fn main() { macro_rules! write { ($dst:expr, $($arg:tt)*) => ($dst.write_fmt($crate::format_args!($($arg)*))) } }
要使用它時,你要輸入這些:
- 一個表達式 (
expr
) 用來得到變數名$dst
。 - 之後的所有東西。如果它寫的是
$arg:tt
,那麼它只會接受一個元素,但因為它寫的是$($arg:tt)*
,所以它可以接受零、一個或者任意多個。
然後它接受 $dst
,並對它呼叫了叫做 write_fmt
的方法。在那裡面,它使用了另一個叫做 format_args!
的巨集來接受所有的 $($arg)*
,或者說我們放進去的全部引數。
現在我們來看一下 todo!
這個巨集。當你想讓程式能編譯但你的程式碼還沒寫出來時,這就是你會用到的那個巨集。看起來像這樣:
#![allow(unused)] fn main() { macro_rules! todo { () => (panic!("not yet implemented")); ($($arg:tt)+) => (panic!("not yet implemented: {}", $crate::format_args!($($arg)+))); } }
這個有兩個選項:你可以輸入 ()
,也可以輸入一些標記樹 (tt
)。
- 如果你輸入的是
()
,它只是使用加上訊息的panic!
。所以其實你可以直接寫panic!("not yet implemented")
,而不是todo!
,結果也一樣。 - 如果你輸入一些引數,它會嘗試印出它們。你可以見到裡面有同樣的
format_args!
巨集,它的工作原理和println!
一樣。
所以如果你寫成這樣,一樣也行得通:
fn not_done() { let time = 8; let reason = "lack of time"; todo!("Not done yet because of {}. Check back in {} hours", reason, time); } fn main() { not_done(); }
會印出:
thread 'main' panicked at 'not yet implemented: Not done yet because of lack of time. Check back in 8 hours', src/main.rs:4:5
在巨集裡面你甚至可以呼叫相同的巨集。這裡是這樣的範例:
macro_rules! my_macro { () => { println!("Let's print this."); }; ($input:expr) => { my_macro!(); }; ($($input:expr),*) => { my_macro!(); } } fn main() { my_macro!(vec![8, 9, 0]); my_macro!(toheteh); my_macro!(8, 7, 0, 10); my_macro!(); }
這個巨集接受 ()
、或一個表達式、或很多個表達式都可以。但是不論你放了什麼,它都會忽略所有的表達式,並且最後只呼叫 my_macro!
的 ()
。所以四次輸出都只是 Let's print this
。
在 dbg!
巨集中也可以看到同樣的情況,也就是呼叫自己。
#![allow(unused)] fn main() { macro_rules! dbg { () => { $crate::eprintln!("[{}:{}]", $crate::file!(), $crate::line!()); // $crate 的意思是指本身所在的 crate. }; ($val:expr) => { // 這裡 `match` 的使用是有意的因為它影響了暫存變數的 // 生命週期 - https://stackoverflow.com/a/48732525/1063961 match $val { tmp => { $crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp); tmp } } }; // 單一引數的後緣逗號會被忽略 ($val:expr,) => { $crate::dbg!($val) }; ($($val:expr),+ $(,)?) => { ($($crate::dbg!($val)),+,) }; } }
eprintln!
與println!
相同,除了它印出到io::stderr
而不是io::stdout
。當然也有個eprint!
印出時不會加上換行。
所以我們可以自己去試一試。
fn main() { dbg!(); }
這與第一分支相匹配,所以它會用 file!
和 line!
巨集印出檔名和行數。印出 [src/main.rs:2]
。
讓我們用這個來試試:
fn main() { dbg!(vec![8, 9, 10]); }
這將會匹配到下一個分支,因為它是個表達式。然後它將把輸入叫做 tmp
並使用這段程式碼:$crate::eprintln!("[{}:{}] {} = {:#?}", $crate::file!(), $crate::line!(), $crate::stringify!($val), &tmp);
。所以它會用 file!
和 line!
來印出,再把 $val
做成 String
,並且用 {:#?}
來給 tmp
做漂亮列印。所以對於我們的輸入,它會寫成這樣:
[src/main.rs:2] vec![8, 9, 10] = [
8,
9,
10,
]
剩下的部分,即使你加了額外的逗號,它也只是對自己呼叫 dbg!
。
正如你所見,巨集是非常複雜的!通常你只想讓巨集自動做些簡單函式無法做得很好的事情。學習巨集的最佳方法就是看看其他巨集的例子。沒有多少人能夠快速寫出巨集而不出問題。所以在 Rust 中,不用認為你需要知道巨集的一切才能知道如何撰寫。但如果你讀了其他巨集,並稍加修改,你就可以很容易地借用它們的威力。之後你可能就會開始習慣寫出自己的巨集。
第二部 - 電腦上的 Rust
你見到了我們可以只使用 Playground 就學習到 Rust 裡的幾乎任何東西。但到目前為止如果你已經學了這麼多,現在你也許會想要在你的電腦上使用 Rust。總有一些事情是你沒辨法用 Playground 做到的,比如使用檔案或在多個檔案中的程式碼。也有一些其它東西需要在電腦上安裝 Rust 的是輸入功能和 flags。但最重要的事是在你的電腦上有了 Rust,你可以使用 Crate。我們已經學過 Crate ,但在 Playground 中你只能使用最流行的那一個。但在你的電腦上有了 Rust,你就可以在你的程式中使用任何 Crate。