iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0

當你剛開始寫程式時,可能一切都很簡單:幾行程式碼,一些函數,事情就能順利運作。但隨著專案變大、邏輯變複雜,事情開始變得沒那麼簡單了。你可能發現自己不斷寫相似的程式碼,手動維護這些重複的邏輯不僅麻煩,還容易出錯。

問題的出現:重覆的程式碼

例如,假設你在開發一個系統,需要根據不同條件輸出不同的錯誤訊息。起初,你可能會寫這樣的程式碼:

fn display_error_404() {
    println!("Error 404: Not Found");
}

fn display_error_500() {
    println!("Error 500: Internal Server Error");
}

fn display_error_403() {
    println!("Error 403: Forbidden");
}

fn main() {
    display_error_404();
    display_error_500();
    display_error_403();
}

這個範例中,雖然每個函數的輸出不同,但邏輯幾乎完全一樣。當專案變大,錯誤類型變多時,你不得不不斷重覆撰寫這樣的程式碼。不僅浪費時間,還容易因為手動維護而出錯。如果要修改所有錯誤訊息的格式,你可能需要修改多個函數,容易造成疏漏。

引入巨集的時機

這時你會想:「有沒有什麼辦法可以自動處理這些重複的工作?」這就是 Rust 巨集 登場的時刻!

巨集是一種在編譯時期自動生成重複程式碼的工具。你可以想像它是一個自動化的工廠,幫你生產出特定的程式碼片段。這個工廠運行的規則(即 SOP)是由你定義的,而它能生產的「產品」可以是函數、結構體、錯誤處理邏輯等。這讓你可以輕鬆應對重複性的程式碼場景。


一、什麼是 Rust 巨集?為什麼需要它?

基本概念:巨集是自動生成程式碼的工具

你可以把巨集想像成一個在編譯時期就幫你寫好程式碼的工具。它允許你提前定義一些模板,根據不同的輸入自動生成相應的程式碼,這樣你就不需要手動撰寫那些重複的邏輯。

範例:重寫錯誤處理邏輯

讓我們看看如何用巨集來簡化前面的錯誤處理程式碼:

// 定義一個巨集,用來自動生成錯誤處理函數
macro_rules! create_error_function {
    ($name:ident, $code:expr, $message:expr) => {
        fn $name() {
            println!("Error {}: {}", $code, $message); // 印出錯誤代碼和訊息
        }
    };
}

// 使用巨集來創建不同的錯誤處理函數
create_error_function!(display_error_404, 404, "Not Found");
create_error_function!(display_error_500, 500, "Internal Server Error");
create_error_function!(display_error_403, 403, "Forbidden");

fn main() {
    // 呼叫自動生成的錯誤處理函數
    display_error_404();  // 顯示 404 錯誤
    display_error_500();  // 顯示 500 錯誤
    display_error_403();  // 顯示 403 錯誤
}

透過這個巨集,我們可以避免撰寫重複的錯誤處理函數,並且只需要一次定義即可生成多個不同的函數。

使用巨集的優點:減少重複工作

巨集允許你一次撰寫,重複生成,這在專案變大時非常有用。舉個日常生活的例子:想像你在餐廳裡點餐,巨集就像是一台自動點餐機,根據你提供的需求(像是菜名),它會自動生成對應的訂單,送到廚房。這樣,你就不用每次都手動寫下點菜單,系統會自動幫你生成菜單。


二、巨集的基本使用方式

在 Rust 中,巨集的定義方式是使用 macro_rules!,並且能處理各種不同類別的參數。以下是巨集的基本模板:

macro_rules! macro_name {
    ( $pattern:pat => $expansion:expr ) => {
        // 展開的程式碼
    };
}
  • macro_name:這是巨集的名稱,之後你可以通過這個名稱來呼叫巨集。
  • $pattern:這是巨集接受的參數。
  • $expansion:這是巨集展開後的程式碼,也就是巨集會生成的內容。
  • $pattern:patpat 代表模式,用於巨集中進行模式匹配。模式在 Rust 中常用來解構資料結構,或匹配特定的值。

參數匹配模式

巨集允許你根據不同的模式進行匹配並生成程式碼。以下是一些常見的匹配模式:

符號 描述 用途範例
ident 匹配識別符,像是函數名稱或變數名稱。 foo, bar 等函數名稱或變數名稱
expr 匹配任意的表達式。 1 + 2, "Hello", vec![1, 2, 3] 等表達式
ty 匹配型別。 i32, String, &str 等型別
block 匹配用 {} 包裹的一段程式碼區塊。 { println!("Hello!"); }
pat 匹配模式,用於模式解構的情境。 Some(x), None, Ok(value) 等匹配模式
path 匹配路徑,用來表示模組路徑或函數路徑。 std::io::Result, self::module
lit 匹配字面值。 數字 42, 字串 "Hello" 等字面值
meta 匹配元數據項,用於特徵或屬性。 #[derive(Debug)]
item 匹配任意程式項目。 fn, struct, enum
stmt 匹配一個陳述句。 let x = 5;, x += 1;
vis 匹配可見性修飾符。 pub, pub(crate)
tt 匹配任意語法樹(Token Tree),是最通用的匹配符號。 任意 Rust 語法元素

在巨集定義中,我們通常會設計多個匹配模式來處理不同的輸入情況,然後展開對應的程式碼。這些匹配模式可以基於傳入的參數形式來決定使用哪個分支。這裡的「模式」其實是指巨集中不同的 模式分支,用來判斷何時應該展開特定的巨集分支。

例子:根據傳入參數展開不同的巨集分支

macro_rules! match_example {
    // 匹配一個單一表達式
    ($val:expr) => {
        println!("匹配到一個單一表達式: {}", $val);
    };

    // 匹配兩個表達式
    ($val1:expr, $val2:expr) => {
        println!("匹配到兩個表達式: {}, {}", $val1, $val2);
    };

    // 匹配識別符(變數名稱)
    ($id:ident) => {
        println!("匹配到識別符: {}", stringify!($id));
    };
}

fn main() {
    match_example!(42);
    match_example!(42, 58);
    match_example!(my_variable);
}

巨集匹配模式解釋:

  1. $val:expr

    • 這表示匹配一個表達式(例如:數字或算式),如果傳入一個表達式,它將展開第一個分支。
  2. $val1:expr, $val2:expr

    • 當傳入兩個表達式時,它會匹配這個分支並展開對應的程式碼。
  3. $id:ident

    • 這是用來匹配識別符(例如:變數名稱),並展開對應的分支。

輸出結果:

匹配到一個單一表達式: 42
匹配到兩個表達式: 42, 58
匹配到識別符: my_variable

這是如何判斷的?

巨集系統根據你提供的參數形式,逐步嘗試匹配最符合的模式:

  • 如果你傳入一個表達式,它會匹配到第一個分支($val:expr)。
  • 如果你傳入兩個表達式,它會匹配到第二個分支($val1:expr, $val2:expr)。
  • 如果你傳入識別符,它會匹配到第三個分支($id:ident)。

對於巨集當中的參數匹配模式部分,會需要一段時間熟悉,下面我們繼續來看關於巨集的其他範例吧。


三、使用巨集的實際好處

1. 減少重複性代碼的工作量

當你需要反覆寫相似的程式碼時,巨集能大大減少你的工作量。例如,讓我們用巨集來處理任意數量的求和操作:

macro_rules! sum {
    ( $( $x:expr ),* ) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };
}

fn main() {
    let result = sum!(1, 2, 3, 4, 5);
    println!("總和是: {}", result);
}

在這段程式碼中,我們定義了一個名為 sum 的巨集,它能接受任意數量的表達式作為輸入,並將這些數值加總後返回結果。巨集的語法部分使用了 $( $x:expr ),*,這是一個模式匹配語法,具體解釋如下:

  1. $x:expr:這裡的 $x 是巨集中的參數,expr 則是表示表達式(expression)的模式,代表我們可以傳入任何合法的 Rust 表達式。像是數字、算式等都可以作為表達式傳入。例如,12 + 3 都屬於表達式。

  2. $( ... ):這部分表示一個重複的模式匹配,它允許多次匹配相同的模式。在這裡,$( $x:expr ) 意味著可以匹配多個表達式,並將每個表達式依次捕捉到 $x 變數中。

  3. *:這是用來表示重複次數的符號,代表「零次或多次」,也就是說你可以傳入一個或多個表達式。

  4. ,*:這部分表示每個表達式之間用逗號(,)分隔。這樣你可以像呼叫 sum!(1, 2, 3, 4, 5) 一樣傳入多個數字,並用逗號隔開。

這個巨集能夠處理任意數量的參數,並將它們相加。這樣的程式碼幫助你避免手動撰寫多個不同的求和函數,不論你有多少數字需要加總,都可以通過同一個巨集來完成。

2. 為結構體初始化建立巨集

巨集允許你根據不同的參數動態生成不同的程式碼。例如,假設你想要根據輸入的類型自動生成 getter 和 setter 方法,這樣每當你定義一個結構體(struct)時,你不必手動撰寫重複的 getter 和 setter 函數。我們可以用巨集來自動完成這項工作:

// 定義一個巨集,用來生成 getter 和 setter 函數
macro_rules! create_getter_setter {
    // 巨集接受一個結構名稱 ($struct_name),以及屬性名稱 ($field_name) 和屬性類型 ($field_type)
    ($struct_name:ident, $field_name:ident, $field_type:ty) => {
        impl $struct_name {
            // 自動生成 getter 函數
            pub fn $field_name(&self) -> &$field_type {
                &self.$field_name
            }

            // 自動生成 setter 函數
            pub fn set_$field_name(&mut self, value: $field_type) {
                self.$field_name = value;
            }
        }
    };
}

// 定義一個結構體 Person,包含 name 和 age 屬性
struct Person {
    name: String,
    age: u32,
}

// 使用巨集自動為 Person 結構體生成 getter 和 setter 函數
create_getter_setter!(Person, name, String);
create_getter_setter!(Person, age, u32);

fn main() {
    let mut person = Person {
        name: "Alice".to_string(),
        age: 30,
    };

    // 呼叫自動生成的 getter 函數
    println!("Name: {}", person.name());
    println!("Age: {}", person.age());

    // 呼叫自動生成的 setter 函數
    person.set_name("Bob".to_string());
    person.set_age(35);

    println!("Updated Name: {}", person.name());
    println!("Updated Age: {}", person.age());
}

在這個例子中,我們使用了一個名為 create_getter_setter 的巨集來自動生成一個結構體的 getter 和 setter 函數。巨集能根據傳入的結構名稱、屬性名稱和屬性類型自動生成對應的方法,這展示了巨集的高度靈活性和模板設計能力。


四、巨集的實際應用場景

自動生成結構方法:操作向量

這個範例展示如何使用巨集來自動為結構體生成對向量的操作方法。假設你有一個 Vector2D 結構體,表示二維向量,你可能需要為這個結構體生成許多操作向量的基本方法,比如加法、減法等。通過巨集,我們可以簡化這些重複性的代碼。

// 定義一個巨集來為 Vector2D 自動生成向量操作方法
macro_rules! create_vector_methods {
    // 匹配方法名稱 ($method)、運算符 ($op),以及對應的運算符號 ($symbol)
    ($method:ident, $op:tt) => {
        impl Vector2D {
            // 為 Vector2D 結構體生成對應的方法
            pub fn $method(&self, other: &Vector2D) -> Vector2D {
                Vector2D {
                    x: self.x $op other.x,
                    y: self.y $op other.y,
                }
            }
        }
    };
}

// 定義 Vector2D 結構體
struct Vector2D {
    x: f64,
    y: f64,
}

// 使用巨集自動生成加法和減法方法
create_vector_methods!(add, +);
create_vector_methods!(subtract, -);

fn main() {
    let vec1 = Vector2D { x: 1.0, y: 2.0 };
    let vec2 = Vector2D { x: 3.0, y: 4.0 };

    // 使用自動生成的加法方法
    let result_add = vec1.add(&vec2);
    println!("加法結果: ({}, {})", result_add.x, result_add.y);

    // 使用自動生成的減法方法
    let result_subtract = vec1.subtract(&vec2);
    println!("減法結果: ({}, {})", result_subtract.x, result_subtract.y);
}
  1. 自動生成多個操作方法:這個巨集展示了如何根據參數自動生成不同的向量操作方法,例如加法(add)和減法(subtract)。
  2. 使用運算符參數化:通過使用 $op:tttt 表示符號或運算符),巨集能夠處理不同的運算符號,動態生成不同的運算邏輯,這讓程式更具彈性。
  3. 巨集的模板功能:巨集可以根據傳入的名稱和運算符自動生成對應的程式碼,這展示了巨集強大的模板化功能,避免手動撰寫重複的操作方法。

這個例子展示了巨集的靈活性,讓你能夠根據不同的需求自動生成多個函數,不需要手動重複撰寫操作邏輯。這樣的方式在處理多個類似操作時特別有用,比如矩陣運算、數學操作、甚至自定義的邏輯處理。


總結

學習 Rust 巨集後,你應該會發現它是一個非常方便的工具,能夠幫助你處理那些重複又枯燥的工作。當專案變大、程式碼變複雜時,巨集可以幫助你自動生成程式碼,讓你不需要手動重複撰寫相似的邏輯。

巨集的好處不只是在省時省力,它還能夠讓你的程式碼更加簡潔、容易維護,特別是在處理重複邏輯或樣板代碼時,巨集就像你的「程式碼生產工廠」,你只需要設計好規則,它就會幫你自動生成你需要的程式碼。

簡單來說:

  • 巨集能幫你自動生成程式碼,減少重複撰寫的麻煩。
  • 巨集讓你的程式碼更加簡潔,容易維護。
  • 它特別適合大型專案中,幫助自動生成許多類似邏輯的程式碼,避免手動維護。

當然,看標題就能知道了,這還只是Rust巨集的其中一部分,接下來我們將會介紹更多關於 Rust 的巨集的進階功能。


上一篇
[Day 20] Rust 與 Python 的 FFI 互通操作指南
下一篇
[Day 22] 淺談 Rust 巨集(二):不再重覆製造輪子
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言