在上一個章節中,我們學習了許多跟文字格式化有關的巨集。在這個章節,我們將會更深入地了解巨集到底是什麼東西,以及實作巨集的方式。



Rust程式語言的巨集,很像是函數,但是它比函數還要更有彈性,而且複雜很多。「巨集」是一個用程式碼產生程式碼的方式,又稱為「元編程」(Metaprogramming),妥善運用可以大量地減少我們需要撰寫的程式碼。當然我們如果能用函數解決的問題就儘量使用函數,因為過度使用巨集通常會使得程式不易閱讀。

crate中的巨集並不像函數一樣,可以直接使用「extern crate」關鍵字來引用進來,必須要搭配「#[macro_use]」記號才行。舉例來說:

以上程式會將「serde」這個crate中的巨集引用進目前程式的scope中。而且如果沒有用這樣的方式來引用巨集的話,是沒辦法如函數直接透過指定命名空間的方式來使用的。

Rust程式語言的巨集分為兩種,分別是「宣告式巨集」(Declarative Macro)和「程序式巨集」(Procedural Macros)。

宣告式巨集

宣告式巨集的用途比程序式巨集還要廣泛,我們可以使用「macro_rules!」巨集來定義並實作新巨集。先來看看Rust程式語言的「vec!」這個巨集是怎麼用「macro_rules!」巨集實作出來的吧!

先提醒一下,以上只是簡化版的「vec!」巨集,Rust程式語言的標準函式庫並不是這樣設計的哦!

有沒有發現?巨集的主體,十分類似「match」關鍵字組成的區塊,似乎也是在做型樣匹配。的確,只不過這邊的匹配方式跟我們之前學到的型樣匹配差異很大,現在的我們幾乎完全看不懂!

先不要灰心,注意到「vec!」巨集上方的「#[macro_export]」記號了嗎?只有使用「#[macro_export]」記號的巨集才可以被「#[macro_use]」記號引用,有點類似我們之前學的「pub」關鍵字,可以控制巨集的存取權限。

接著看到「vec!」巨集的主體,這個主體只有使用一條匹配的arm,匹配樣本中的「$x:expr」表示要匹配Rust程式語言的表達式,並且將它命名為「$x」。「*」表示要匹配0個以上的「$( $x:expr )」。也就是說,「( $( $x:expr ),* )」可以匹配到Rust程式語言的小括號「()」、中括號「[]」或是大括號「{}」,並且「,」表示「*」是依據逗號「,」來分隔每個「$x:expr」。

再來看一下這條arm的程式敘述區塊,第6行到第8行,「$()*」表示一個迴圈結構,類似把匹配到的多個「$x」,按照順序走訪完,並把每次迭代的值給「$x」使用。至於這條arm的程式敘述區塊會什麼需要再多加一層程式敘述區塊,這是因為要讓「vec!」巨集回傳的值並不是Vec結構實體,而是產生Vec結構實體的程式碼!

「main」函數中,程式在編譯「vec!」巨集時,編譯器會先解開巨集,看它實際的Rust程式碼到底是什麼。解開「vec!」巨集後的程式碼如下:

程序式巨集

程序式巨集更像是函數,它允許輸入Rust的程式碼,對這個程式碼做處理來產生出新的程式碼,不像是宣告式巨集只能利用型樣匹配來透過一些參數和已經寫好的程式碼範本來產生程式碼。當然,程序式巨集的實作方式也會比宣告式巨集複雜許多。先前我們使用過的「#[derive(Debug)]」就是一種程序式巨集的用法。我們可以把任意的特性當作程序式巨集來使用,舉例來說:

我們希望以上程式碼可以輸出:

Hello world, my name is Pineapple!
Hello world, my name is Durian!

為了要讓「HelloWorld」特性可以作為程序式巨集來使用,我們必須將它另外使用一個獨立的crate來實作。我們可以建立一個名為「hello_world_derive」的函式庫專案,接著修改該專案的「Cargo.toml」,添加以下內容:

「syn」和「quote」這兩個套件可以幫助我們處理Rust的程式碼,等等會用到。「lib」區塊中的「proc-macro」可以設定這個程式專案是否為程序式巨集的專案。

接著在「lib.rs」檔案中,撰寫以下程式:

以上程式,「proc_macro_derive」這個記號屬性可以綁定某個特性名稱和函數,使其可以搭配「derive」記號屬性使用,來產生出Rust的程式碼。「proc_macro」這個套件是Rust程式語言內建的crate,它可以將Rust的程式碼轉成字串來處理。「syn」套件可以將Rust的程式碼字串轉成「DeriveInput」結構實體,方便我們去處理Rust的程式碼。「quote」套件可以接受「syn」套件轉出來的「DeriveInput」結構實體,將其轉回Rust的程式碼。

程式第12行,將Rust的程式碼轉成字串。程式第15行,將Rust的程式碼字串轉成特定的資料結構。程式第18行,另外呼叫了「impl_hello_world」函數來產生出使用「derive」記號屬性時要產生出來的Rust程式碼。程式第21行,將產生出來的Rust程式碼回傳。

「DeriveInput」的「ident」為使用「derive」記號屬性的結構體或是列舉名稱,如果是「Pineapple」結構體,名稱就會是「Pineapple」。程式第25行,宣告了變數「name」來儲存使用「derive」記號屬性的結構體或是列舉名稱。緊接著,「quote!」這個巨集是「quote」套件提供的,這個巨集十分方便,我們可以直接將要產生出來的Rust程式碼寫在這個巨集的程式敘述區塊內。

在「quote!」巨集的程式敘述區塊中,可以直接使用「#」接上外面的變數名稱,就可以用外面的變數名稱所儲存的值,來取代「quote!」巨集的程式敘述區塊中的「#」接上變數名稱所形成的空格。程式第29行所使用的「stringify!」巨集是Rust程式語言的標準函式庫提供的,可以在程式編譯階段直接將Rust的表達式直接轉成字串定數,而不會去執行它。

將「hello_world_derive」函式庫專案的程式寫好之後,就可以使用我們原本的可執行程式專案來引用啦!改寫後的程式如下:

程式在編譯階段時,有用到程序式巨集的部份會被展開成:

程式執行結果如下:

Hello, World! My name is Pineapple
Hello, World! My name is Durian

結論

Rust的巨集功能十分強大,然而我們現階段想要上手,並隨意地做出自己的巨集,光是參考這個章節的內容是遠遠不夠的。但是我們現在對於巨集是如何工作的,已經有了足夠的認知,將來在使用標準函式庫中的巨集,或是使用由其它Rust開發者所提供的巨集時,也會更加知道我們的程式在編譯階段時究竟發生了什麼!

下一章節,我們要來學習Rust程式語言的不安全設計方式!

下一章:不安全的Rust