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

內容解密:

  1. 巨集追蹤功能的啟用:透過 #![feature(trace_macros)] 屬性啟用巨集追蹤功能。
  2. 使用 trace_macros! 巨集:使用 trace_macros! 巨集來控制巨集追蹤功能的開關。
  3. cargo expand 指令:使用 cargo expand 指令來檢視巨集展開後的程式碼。

編寫新的巨集

接下來,我們將編寫一個新的巨集來示範更多的功能。這個巨集將接受任意數量的識別符號,並以 name=value 的形式輸出它們的值。這對於除錯非常有用。

巨集定義

macro_rules! var_print {
    ($($v:ident),*) => {
        println!(
            concat!($(stringify!($v),"={:?} "),*), $($v),*
        );
    };
}

內容解密:

  1. macro_rules!:用於定義巨集的語法。
  2. $($v:ident),*:匹配一個以逗號分隔的識別符號列表。
  3. stringify!($v):將識別符號轉換為字串。
  4. concat!:將多個字串連線成一個字串。
  5. $(stringify!($v),"={:?} "),*:為每個識別符號生成一個 name=value 的字串,並將它們連線起來。
  6. $($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),
        ],
    ));
};

內容解密:

  1. var_print! 巨集的展開:將 var_print! 巨集展開為 println! 的呼叫。
  2. concat! 的作用:將多個字串連線成一個格式字串。
  3. $($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)*);
    };
    () => ()
}

內容解密:

  1. lazy_static 巨集的匹配模式:匹配兩種可能的模式,分別是 static ref NAME: TYPE = EXPR;pub static ref NAME: TYPE = EXPR;
  2. $(#[$attr:meta])*:允許在靜態變數上新增屬性。
  3. $($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,包含nameagebreed三個欄位。
  • 實作了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();