在上一篇文章中,我們探討了 Rust 巨集的基本概念及其如何幫助減少程式碼的重複。現在,我們將進一步了解巨集在 Rust 中的分類,並深入討論 程序式巨集 的主題。
在 Rust 中,巨集主要可以分為三種類型:
宣告式巨集(Declarative Macros):
macro_rules!
定義,通過模式匹配自動生成程式碼。它們對於處理重複的邏輯非常有用。例如,可以用來自動生成函數或方法,簡化重複的程式碼。程序式巨集(Procedural Macros):
屬性巨集(Attribute-like Macros):
接下來,我們將專注於 程序式巨集,並探討其工作原理、使用方式及實際應用場景。
程序式巨集讓你能用 Rust 程式碼來寫自己的程式碼生成器,和宣告式巨集不一樣,程序式巨集給你更多的自由和靈活性。它允許你寫下任何 Rust 程式碼,然後在需要的時候自動生成新的程式碼。
你可以把它和 Python 的 exec
函數作比較。兩者都能動態生成和執行程式碼,提供豐富的靈活性。不同的是,exec
是在運行時執行的,而程序式巨集則是在編譯時期處理,這意味著生成的程式碼會在編譯過程中進行靜態檢查,讓你的程式更安全、更高效。因此,程序式巨集的強大之處在於它的靈活性和可擴展性,讓你可以針對特定需求隨意調整生成的程式碼。
使用以下命令創建一個新的 Rust 專案:
cargo new hello_macro
cd hello_macro
這將創建一個名為 hello_macro
的新專案,並自動進入該目錄。
Cargo.toml
檔案打開 Cargo.toml
檔案,添加 proc-macro
特性以啟用程序式巨集支持。在 [lib]
部分下添加以下內容:
[lib]
proc-macro = true
最終的 Cargo.toml
應該看起來像這樣:
[package]
name = "hello_macro"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
現在,打開 src/lib.rs
檔案,並將其內容替換為以下程式碼:
// 在 lib.rs 中定義程序式巨集
extern crate proc_macro; // 引入 proc_macro crate
use proc_macro::TokenStream; // 引入 TokenStream 結構
// 定義一個程序式巨集
#[proc_macro]
pub fn hello_world(_input: TokenStream) -> TokenStream {
// 生成的程式碼
"fn hello() { println!(\"Hello, world!\"); }".parse().unwrap()
}
這段程式碼的功能如下:
extern crate proc_macro;
引入 Rust 的內建 proc_macro
,這是處理程序式巨集所需的。use proc_macro::TokenStream;
引入 TokenStream
結構,這是用來表示 Rust 程式碼結構的類型。TokenStream 是一種資料類別,用來表示 Rust 程式碼的語法樹(syntax tree)。hello_world
的程序式巨集,這個巨集不接受任何輸入,並返回一段 Rust 的程式碼。hello
函數,它會打印 "Hello, world!"。打開 src/main.rs
檔案,並添加以下程式碼:
// 在 main.rs 中使用程序式巨集
use hello_macro::hello_world; // 從 hello_macro 專案中引入我們的程序式巨集
fn main() {
hello_world!(); // 調用程序式巨集,這將生成並執行 `hello` 函數
hello(); // 這將打印 "Hello, world!"
}
使用以下命令編譯並運行應用程式:
cargo run
如果一切正常,你應該會看到輸出:
Hello, world!
可以看出上面的範例中,當我們用 hello_world!()
呼叫巨集後,它實際上會產生一段 Rust 程式碼,生成的程式碼包含一個被定義的 hello()
函數,以便於後面的程式運行當中都可以使用該函數。這是一個簡單的範例,接下來我們再介紹一些進階的應用方法。
程序式巨集可以在多種情境下使用,以下是一些典型的應用範例:
自動生成程式碼:
DSL(領域特定語言):
測試生成:
特徵實現:
上面的範例當中,我們並未加入任何參數,現在我們要嘗試在建立程序式巨集的時候,加入參數來動態改變生成的程式碼內容。
打開 src/lib.rs
檔案,並將其內容替換為以下程式碼:
// 在 lib.rs 中定義程序式巨集
extern crate proc_macro; // 引入 proc_macro crate
use proc_macro::TokenStream; // 引入 TokenStream 結構
use quote::quote; // 引入 quote 巨集,用於生成 Rust 程式碼
use syn::{parse_macro_input, Ident}; // 引入所需的類型
// 定義一個名為 MyAdd 的程序式巨集
#[proc_macro]
pub fn my_add(input: TokenStream) -> TokenStream {
// 解析輸入的 TokenStream 以獲取函數名稱
let input_name = parse_macro_input!(input as Ident);
// 生成加法函數的程式碼
let gen = quote! {
fn #input_name(a: i32, b: i32) -> i32 {
a + b
}
};
gen.into() // 返回生成的程式碼
}
引入必要的 套件:
extern crate proc_macro;
引入 Rust 的內建 proc_macro
,這是編寫程序式巨集的基礎。use proc_macro::TokenStream;
使我們能夠使用 TokenStream
這個資料型別,這是處理傳入和返回程式碼的核心結構。use quote::quote;
引入 quote
巨集,這讓我們能方便地生成 Rust 程式碼。use syn::{parse_macro_input, Ident};
引入 syn
crate 的相關類型,以便於解析輸入的程式碼,特別是獲取函數名稱。定義程序式巨集:
#[proc_macro]
標記來告訴 Rust 編譯器這是一個程序式巨集。my_add
接收一個 TokenStream
作為輸入,並返回一個新的 TokenStream
。let input = parse_macro_input!(input as Ident);
解析傳入的 TokenStream
,將其轉換為一個識別符(Ident
),這樣我們就可以提取函數名稱。quote!
巨集生成一段 Rust 程式碼,這段程式碼實現了加法邏輯。具體來說,它將創建一個名為 input_name
的函數,該函數可以接收兩個整數參數,並返回它們的和。打開 src/main.rs
檔案,並添加以下程式碼以使用我們定義的程序式巨集:
// 在 main.rs 中使用程序式巨集
use hello_macro::my_add; // 引入 hello_macro 專案中自定義的程序式巨集 my_add
// 呼叫程序式巨集來生成名為 add 的加法函數
my_add!(add); // 這將生成一個函數 `add`
fn main() {
let sum = add(5, 3); // 調用自動生成的加法函數
println!("5 + 3 = {}", sum); // 輸出結果
}
引入程序式巨集:
use hello_macro::my_add;
將我們剛剛定義的程序式巨集引入到主函數所在的檔案中,這樣就可以在 main
函數中使用它。生成加法函數:
my_add!(add);
,這行程式碼將會觸發 my_add
巨集的執行,生成一個名稱為 add
的加法函數。這個函數的內容是自動生成的,不需要手動編寫。使用自動生成的加法函數:
main
函數中,我們可以直接使用 add(5, 3)
來調用我們生成的加法函數,並將結果存儲在 sum
變數中。println!
輸出加法的結果,顯示 5 + 3 = 8
。這樣,我們就完成了一個完整的流程,利用程序式巨集自動生成了加法函數並進行計算。在終端中,使用以下命令編譯並運行應用程式:
cargo run
如果一切正常,你應該會看到類似以下的輸出:
5 + 3 = 8
在這個範例中,我們成功地使用程序式巨集來自動生成一個加法函數。這樣,我們可以在不同的地方調用這個函數,而無需每次都手動編寫加法邏輯。透過這樣的方式,當你需要進行相似的計算時,可以大幅提高開發效率,同時保持程式碼的整潔性。
在這個範例中,我們將定義一個簡單的屬性巨集,用來自動為結構體添加一個打印方法。這樣,我們可以很方便地使用該方法來輸出結構體的內容。透過這個過程,我們將學會如何利用屬性巨集來簡化程式碼並提高開發效率。
首先,打開 src/lib.rs
檔案,並將其內容替換為以下程式碼:
extern crate proc_macro; // 引入 proc_macro crate
use proc_macro::TokenStream; // 引入 TokenStream 結構
use quote::quote; // 引入 quote 巨集,用於生成 Rust 程式碼
use syn::{parse_macro_input, DeriveInput}; // 引入所需的類型
// 定義一個名為 Print 的屬性巨集
#[proc_macro_derive(Print)]
pub fn print_derive(input: TokenStream) -> TokenStream {
// 解析輸入的 TokenStream,以獲取結構體的名稱
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident; // 獲取結構體的識別符
// 生成打印方法的程式碼
let gen = quote! {
impl #name {
pub fn print(&self) {
// 直接打印結構體的欄位
let name = &self.model; // 假設結構體有一個 `model` 欄位
let year = self.year; // 假設結構體有一個 `year` 欄位
println!("Model: {}, Year: {}", name, year); // 使用自定義格式打印
}
}
};
gen.into() // 返回生成的程式碼
}
引入必要的庫:使用 extern crate proc_macro;
引入 Rust 的內建 proc_macro
crate,這是編寫屬性巨集的基礎。接下來引入 TokenStream
和 quote
庫,以便於生成程式碼結構。
解析輸入:使用 parse_macro_input!(input as DeriveInput);
來解析輸入的程式碼,以獲取結構體的名稱。這使我們可以針對具體的結構體進行操作。DeriveInput
可以視為一種結構體輸入的表示方式,它專門用來表示輸入的結構體或枚舉,其成分包括名稱識別符(ident
)、欄位(fields
)、特徵(attributes
)等資訊。此處以結構體識別符套用至結構體的實作方法上,這樣一來就會適用於各種名稱的結構體。
生成打印方法:利用 quote!
來生成打印方法的實現,並命名為函數 print
。這個方法將直接從結構體的欄位中讀取資料並格式化輸出。
接下來,我們來看看如何在結構體中使用這個屬性巨集。假設我們有一個 Car
結構體,並希望能夠自動生成一個打印方法。透過使用 #[derive(Print)]
,我們可以輕鬆實現這一點:
use hello_macro::Print; // 引入程序式巨集的實現
#[derive(Print)] // 使用屬性巨集
struct Car {
model: String, // 車型
year: u32, // 年份
}
fn main() {
let car = Car {
model: "Toyota".to_string(),
year: 2020,
};
car.print(); // 使用自動生成的 print 方法來輸出車輛資訊
}
導入巨集:在 main.rs
中,使用 use hello_macro::Print;
將我們之前定義的屬性巨集引入。這樣,我們就可以在後面的程式碼中使用它。
定義結構體:使用 #[derive(Print)]
標註 Car
結構體,這樣會自動生成 print
方法,使得我們可以輕鬆地打印這個結構體的內容。
創建實例:在 main
函數中,我們創建了一個 Car
結構體的實例,並用 car.print();
調用自動生成的打印方法來輸出車輛資訊。
在終端中,使用以下命令編譯並運行應用程式:
cargo run
如果一切正常,你應該會看到類似以下的輸出:
Model: Toyota, Year: 2020
在這個範例當中,我們建立了一種屬於 屬性巨集
的 Print
巨集,它可以被使用在對結構體加入一個打印的實作功能,當然這是一個最簡單易懂的例子,可以讓我們看出屬性巨集在引用的時候能為開發所帶來的便利性。
雖然程序式巨集功能強大,但在使用時也需要注意以下幾點:
性能考量:
錯誤處理:
維護性:
程序性巨集在 Python 開發者的經驗當中,可以被理解為類似於 exec
和 @
修飾器的概念,但在 Rust 開發中卻存在一些差異。以下是 Python 和 Rust 的 exec
、修飾器與程序性巨集之間的差異:
特性 | Python (exec 和修飾器) |
Rust (程序性巨集) |
---|---|---|
執行時機 | 運行時執行 | 編譯時處理 |
產生的程式碼 | 動態生成程式碼 | 靜態生成程式碼 |
使用場景 | 通常用於動態行為與裝飾函數 | 用於自動化程式碼生成與特徵實現 |
從此篇與上篇文章內容中,不難看出 Rust 的巨集提供了多種開發效能提升的潛力,但是實務操作上不論是使用宣告式巨集或者程序式巨集,都仍要符合 Rust 開發的語法規則,因此巨集使用可看作是 Rust 程式開發的綜合能力測驗,在越來越熟悉語法規則的情況下,才能用出一朵花來,在這兩篇文章中僅提供簡單的範例,有一個概念式的印象即可,後續更多的延伸實作與練習就讓我們一起加油囉!
在這裡,我們補充一些 Rust 中常用的內建巨集,幫助讀者在開發過程中更有效地使用它們:
println!
:
println!("Hello, {}", name);
// 這樣就可以打印 "Hello, John"(假設 name
是 "John")。print!
:
println!
類似,但不會自動換行。當你不想在訊息後換行時,這個巨集很有用。print!("This is a message.");
// 會打印消息但不換行,適合需要連續輸出的場景。format!
:
let s = format!("Hello, {}", name);
// 會創建一個新的字符串 s
,內容為 "Hello, John"。vec!
:
Vec
,非常方便,讓你可以輕鬆地建立一個可變長度的數組。let numbers = vec![1, 2, 3, 4];
// 這樣就創建了一個包含 1 到 4 的數字的向量。assert!
:
assert!(x > 0, "x must be positive");
// 如果 x
小於或等於 0,就會報錯並顯示消息。debug_assert!
:
assert!
相似,但只在 debug 模式下檢查條件。這讓你在測試期間進行更嚴格的檢查,但在生產模式中不會影響性能。debug_assert!(x < 100);
// 只有在 debug 模式下,若 x
大於或等於 100 會報錯。todo!
:
fn unimplemented_function() { todo!(); }
// 這樣就可以清楚地知道這個函數還沒完成。unimplemented!
:
todo!
類似,但語義略有不同。它在代碼中可以明確表示還沒有開發出某個功能。fn my_function() { unimplemented!(); }
// 這行代碼會顯示函數尚未實現的提示。在 Rust 中,有一些內建的屬性巨集可以用來簡化代碼和提供特定功能。以下是幾個常見的內建屬性巨集的例子:
#[derive]
:
Debug
、Clone
等特徵,省去手動實現的麻煩。#[derive(Debug, Clone, Default, PartialEq)]
struct Person {
name: String,
age: u32,
}
#[repr]
:
#[repr(C)] // 使用 C 語言的佈局
struct MyStruct {
a: i32,
b: f64,
}
#[allow]
和 #[warn]
:
#[allow]
取消特定警告,而 #[warn]
則讓特定的警告變為錯誤,幫助開發者更好地管理代碼質量。#[allow(dead_code)] // 允許未使用的代碼
fn unused_function() {}
#[warn(unused_variables)] // 對未使用的變量產生警告
fn example() {
let x = 42; // 這裡會產生警告
}
#[inline]
:
#[inline]
fn add(x: i32, y: i32) -> i32 {
x + y
}
作用:#[inline] 是一個建議性指令,告訴編譯器「如果可能的話,請將這個函數內聯(inline)」。內聯函數的概念是,編譯器會將函數的實現插入到函數被調用的地方,而不是通過函數調用來執行。這樣可以消除函數調用的開銷,從而提升性能。
使用場景:通常用在經常被調用的簡單函數上,例如數學運算或是小的輔助函數。內聯有助於減少函數調用的開銷,特別是在迴圈中頻繁調用的情況下。
注意事項:內聯不保證一定會發生;編譯器會根據其內部的優化策略決定是否內聯。過多的內聯可能導致編譯後的程式碼體積增大,反而影響性能,所以應謹慎使用。
#[cfg]
:
#[cfg(target_os = "windows")]
fn windows_specific_function() {
// Windows 專用的實現
}
#[cfg(target_os = "linux")]
fn linux_specific_function() {
// Linux 專用的實現
}
作用:#[cfg] 是一個條件編譯巨集,它允許你根據不同的配置條件來包含或排除某些程式碼。在這個例子中,target_os = "windows" 和 target_os = "linux" 是條件檢查,根據編譯的目標操作系統來選擇性地編譯某個函數。
使用場景:當你在寫一個跨平台的應用程式時,可以使用 #[cfg] 巨集來針對不同的操作系統提供不同的實現。例如,某些函數可能只在 Windows 系統下有效,而在 Linux 系統下則不適用。
如何工作:當你使用 cargo build --target 指令編譯你的程式時,Rust 編譯器會根據指定的目標環境檢查這些條件。這樣,只有符合條件的函數才會被編譯,這有助於保持程式碼的清晰和可讀性,並確保你的程式在不同平台上能正常運作。
這些內建的屬性巨集可以幫助你編寫更簡潔、更具可讀性的 Rust 代碼,並能在特定場景下提高開發效率。希望這些例子能幫助你在日常開發中更好地利用 Rust 的強大功能!