Rust 的巨集系統提供強大的元程式設計能力,能有效減少程式碼重複並提升程式碼表達力。透過 nightly 版本的巨集追蹤功能和 cargo-expand
工具,開發者可以深入理解巨集的展開過程,進而編寫更精確的巨集。巨集不僅能用於生成重複的程式碼結構,例如不同狗品種的結構體,也能用於建立迷你 DSL,例如 lazy_static
巨集。在實務上,結合特徵(Trait)的使用,巨集更能展現其彈性,提供統一的介面和行為。由於 Rust 不支援可選函式引數,Builder 模式提供了一個良好的替代方案,讓函式呼叫更具彈性,並維持程式碼的清晰度。
5.1 透過巨集進行元程式設計
在 Rust 中,我們可以透過啟用巨集追蹤功能(僅限 nightly 版本)來觀察巨集的展開過程。首先,我們需要在程式碼中加入以下屬性:
#![feature(trace_macros)]
切換到 Nightly 版本的 Rust
若要使用 nightly 版本的 Rust,可以執行以下指令:
rustup default nightly
或者針對目前專案覆寫工具鏈版本:
rustup override set nightly
此外,也可以透過在執行 cargo
指令時加上 +nightly
引數來使用 nightly 版本的 Rust,例如:
cargo +nightly build
或者在專案根目錄建立一個 rust-toolchain.toml
檔案,內容如下:
[toolchain]
channel = "nightly"
使用巨集追蹤功能
啟用巨集追蹤功能後,我們可以使用 trace_macros!
巨集來觀察特定巨集的展開過程:
trace_macros!(true);
special_println!("hello world!");
trace_macros!(false);
編譯上述程式碼後,編譯器會輸出巨集展開的結果:
note: trace_macro
--> src/main.rs:84:5
|
84 | special_println!("hello world!");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: expanding `special_println! { "hello world!" }`
= note: to `println! ("Printed specially: {}", format! ("hello world!"))`
= note: expanding `println! { "Printed specially: {}", format!("hello world!") }`
= note: to `{
$crate::io::_print($crate::format_args_nl!("Printed specially: {}", format!("hello world!")));
}`
= note: expanding `format! { "hello world!" }`
= note: to `{
let res = $crate::fmt::format($crate::__export::format_args!("hello world!")); res
}`
使用 cargo-expand 檢視巨集展開結果
除了使用巨集追蹤功能外,我們也可以使用 cargo expand
指令來檢視巨集展開後的程式碼。這對於想要在 stable 版本的 Rust 中檢視巨集展開結果的開發者來說非常方便。
首先,需要安裝 cargo-expand
:
cargo install cargo-expand
然後,執行以下指令即可檢視巨集展開後的程式碼:
cargo expand
內容解密:
- 巨集追蹤功能的啟用:透過
#![feature(trace_macros)]
屬性啟用巨集追蹤功能。 - 使用
trace_macros!
巨集:使用trace_macros!
巨集來控制巨集追蹤功能的開關。 cargo expand
指令:使用cargo expand
指令來檢視巨集展開後的程式碼。
編寫新的巨集
接下來,我們將編寫一個新的巨集來示範更多的功能。這個巨集將接受任意數量的識別符號,並以 name=value
的形式輸出它們的值。這對於除錯非常有用。
巨集定義
macro_rules! var_print {
($($v:ident),*) => {
println!(
concat!($(stringify!($v),"={:?} "),*), $($v),*
);
};
}
內容解密:
macro_rules!
:用於定義巨集的語法。$($v:ident),*
:匹配一個以逗號分隔的識別符號列表。stringify!($v)
:將識別符號轉換為字串。concat!
:將多個字串連線成一個字串。$(stringify!($v),"={:?} "),*
:為每個識別符號生成一個name=value
的字串,並將它們連線起來。$($v),*
:將識別符號列表作為引數傳遞給println!
。
測試新的巨集
let counter = 7;
let gauge = core::f64::consts::PI;
let name = "Peter";
var_print!(counter, gauge, name);
執行上述程式碼後,輸出結果如下:
counter=7 gauge=3.141592653589793 name="Peter"
使用 cargo-expand 檢視 var_print! 巨集的展開結果
let counter = 7;
let gauge = 3.14;
let name = "Peter";
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["counter=", " gauge=", " name=", " \n"],
&[
::core::fmt::ArgumentV1::new_debug(&counter),
::core::fmt::ArgumentV1::new_debug(&gauge),
::core::fmt::ArgumentV1::new_debug(&name),
],
));
};
內容解密:
var_print!
巨集的展開:將var_print!
巨集展開為println!
的呼叫。concat!
的作用:將多個字串連線成一個格式字串。$($v),*
的作用:將識別符號列表作為引數傳遞給println!
。
使用巨集建立迷你 DSL
在 Rust 中,我們可以使用巨集來建立迷你領域特定語言(DSL)。lazy_static
crate 就是一個很好的例子。
lazy_static 巨集的定義
macro_rules! lazy_static {
($(#[$attr:meta])* static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
// 使用 `()` 明確轉發私有專案的資訊
__lazy_static_internal!($(#[$attr])* () static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
__lazy_static_internal!($(#[$attr])* (pub) static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub ($($vis:tt)+) static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
__lazy_static_internal!($(#[$attr])* (pub ($($vis)+)) static ref $N : $T = $e; $($t)*);
};
() => ()
}
內容解密:
lazy_static
巨集的匹配模式:匹配兩種可能的模式,分別是static ref NAME: TYPE = EXPR;
和pub static ref NAME: TYPE = EXPR;
。$(#[$attr:meta])*
:允許在靜態變數上新增屬性。$($t:tt)*
:使巨集具有遞迴性,可以處理多個靜態變數的宣告。
透過上述範例,我們可以看到 Rust 中的巨集如何用於元程式設計和建立 DSL。這些技術可以大大提高程式碼的可讀性和可維護性。
5.1 宏在Rust中的應用:超越基礎
在Rust中,宏是一種強大的工具,用於程式碼生成和元程式設計。本章節將探討宏的使用,特別是在實作設計模式和減少重複程式碼方面的應用。
5.1.4 使用宏實作DRY原則
DRY(Don’t Repeat Yourself)原則是軟體開發中的一項基本原則,旨在減少重複的程式碼。宏是實作這一原則的有效工具,尤其是在需要為多個相似的結構或函式生成程式碼時。
例子:為狗的品種生成結構
假設我們需要為數百種狗的品種建立結構。如果手動定義每個結構,將會非常繁瑣且容易出錯。這時,我們可以使用宏來簡化這一過程。
macro_rules! dog_struct {
($breed:ident) => {
struct $breed {
name: String,
age: i32,
breed: String,
}
impl $breed {
fn new(name: &str, age: i32) -> Self {
Self {
name: name.into(),
age,
breed: stringify!($breed).into(),
}
}
}
};
}
dog_struct!(Labrador);
dog_struct!(Golden);
dog_struct!(Poodle);
程式碼解析:
- 宏
dog_struct!
接受一個引數$breed:ident
,代表狗的品種名稱。 - 在宏內部,我們定義了一個結構
$breed
,包含name
、age
和breed
三個欄位。 - 實作了
new
方法,用於建立新的$breed
例項,並將品種名稱儲存在breed
欄位中。
擴充套件結果:
使用cargo expand
命令,我們可以看到宏展開後的結果:
struct Labrador {
name: String,
age: i32,
breed: String,
}
impl Labrador {
fn new(name: &str, age: i32) -> Self {
Self {
name: name.into(),
age,
breed: "Labrador".into(),
}
}
}
// ... 其他品種的結構和實作 ...
新增特徵(Trait)以實作反射
為了進一步利用宏,我們可以為生成的結構實作一個特徵(Trait),以提供統一的介面。
trait Dog {
fn name(&self) -> &String;
fn age(&self) -> i32;
fn breed(&self) -> &String;
}
macro_rules! dog_struct {
($breed:ident) => {
// ... 結構定義 ...
impl Dog for $breed {
fn name(&self) -> &String {
&self.name
}
fn age(&self) -> i32 {
self.age
}
fn breed(&self) -> &String {
&self.breed
}
}
};
}
測試:
let peter = Poodle::new("Peter", 7);
println!("{} is a {} of age {}", peter.name(), peter.breed(), peter.age());
輸出:
Peter is a Poodle of age 7
5.2 可選函式引數
許多程式語言支援可選函式引數,這使得函式呼叫更加靈活。然而,Rust不直接支援可選函式引數。本文將探討Rust中實作類別似功能的方法。
為什麼Rust不支援可選函式引數?
Rust的設計哲學強調明確性和安全性。可選函式引數可能會導致函式簽名不明確,從而增加出錯的可能性。
使用Builder模式實作可選引數
在Rust中,一種常見的方法是使用Builder模式來模擬可選函式引數。
struct Config {
timeout: u32,
retries: u32,
// 其他組態項...
}
struct ConfigBuilder {
timeout: Option<u32>,
retries: Option<u32>,
// 其他組態項...
}
impl ConfigBuilder {
fn new() -> Self {
ConfigBuilder {
timeout: None,
retries: None,
// 初始化其他組態項...
}
}
fn timeout(mut self, timeout: u32) -> Self {
self.timeout = Some(timeout);
self
}
fn retries(mut self, retries: u32) -> Self {
self.retries = Some(retries);
self
}
fn build(self) -> Config {
Config {
timeout: self.timeout.unwrap_or(30), // 預設值
retries: self.retries.unwrap_or(3), // 預設值
// 處理其他組態項...
}
}
}
使用範例:
let config = ConfigBuilder::new()
.timeout(60)
.retries(5)
.build();