Rust 語言設計選擇不支援可選引數和函式過載,以確保與 C 語言相容,並避免潛在問題。然而,開發者可以運用特徵的彈性,結合泛型和特徵約束,巧妙地模擬可選引數的功能。透過定義不同的特徵和對應方法,搭配泛型函式,就能根據不同的特徵約束,呼叫對應的方法,達到類別似可選引數的效果。這種方式雖然不像 C++ 直接,但更具彈性,也更符合 Rust 的設計哲學。此外,建造者模式提供另一種解決方案,它允許開發者逐步構建複雜物件,避免過多的建構子引數,並提升程式碼可讀性。透過定義 Builder 結構體和一系列 setter 方法,開發者可以按需設定物件屬性,最後再呼叫 build 方法,取得完整的物件例項。

5.2.2 檢視C++中的可選引數

C++允許透過函式過載使用可選的函式引數。也就是說,在C++中可以有多個具有不同引數的函式定義,並且這些函式可以為任何缺失的引數提供預設值。這種模式在C++中可能看起來像這樣,有三個過載的函式:

void func() {
    func(true, 11);
}

void func(optional_bool: bool) {
    func(optional_bool, 11);
}

void func(optional_bool: bool, optional_int: int) {
    // ... 函式主體在此 ...
}

內容解密:

此程式碼展示了C++中如何透過函式過載實作可選引數。第一個func()呼叫第二個func(bool),並傳入預設的布林值true和整數11。第二個func(bool)再呼叫第三個func(bool, int),傳入布林值和整數11。這種方式讓呼叫者可以省略某些引數,而由預設值補充。

C++透過對函式名稱進行混淆(mangling)來實作這一點,這使得C++函式與根據C的函式庫不相容。從C++呼叫C程式碼很容易,但反之則最好避免。

5.2.3 Rust中的可選引數或缺乏可選引數

Rust明確地缺乏可選引數或函式過載是一種設計選擇,部分是為了與C相容,部分是為了避免前述章節中提到的批評。然而,我們可以在一定程度上模擬這些功能。我們有三種選擇:

  • 使用特徵(traits)進行擴充套件
  • 使用巨集(macros)在編譯時匹配引數
  • 使用Option包裝引數

我們將重點關注第一種模式:使用特徵進行擴充套件。

5.2.4 使用特徵模擬可選引數

首先,我們將展示可以為一個結構體實作具有衝突方法名稱的兩個特徵:

struct Container {
    name: String,
}

trait First {
    fn name(&self) {}
}

trait Second {
    fn name(&self) {}
}

impl First for Container {
    fn name(&self) {}
}

impl Second for Container {
    fn name(&self) {}
}

內容解密:

此段程式碼定義了一個名為Container的結構體和兩個特徵FirstSecond,這兩個特徵都有一個名為name的方法。然後為Container實作了這兩個特徵。這樣做的結果是,當我們嘗試呼叫container.name()時,編譯器會報錯,因為它無法確定應該呼叫哪個name方法。

接下來,如果特徵方法具有不同的簽名會怎麼樣?讓我們為Second特徵的方法新增一個布林引數:

trait First {
    fn name(&self) {}
}

trait Second {
    fn name(&self, _: bool) {}
}

impl First for Container {
    fn name(&self) {}
}

impl Second for Container {
    fn name(&self, _: bool) {}
}

內容解密:

即使方法簽名不同,編譯器仍然會報錯,因為直接呼叫container.name()仍然存在歧義。

然後,我們可以使用特徵約束來定義兩個函式:

fn get_name_from_first<T: First>(t: &T) {
    t.name()
}

fn get_name_from_second<T: Second>(t: &T) {
    t.name(true)
}

內容解密:

這兩個函式分別呼叫了來自FirstSecond特徵的name方法。透過使用泛型和特徵約束,我們可以告訴編譯器我們想要使用哪個方法。這樣,當我們呼叫get_name_from_first(&container)get_name_from_second(&container)時,編譯器就知道該使用哪個方法了。

5.3 Builder模式

Builder模式是《設計模式》中描述的原始模式之一。這個模式在軟體設計中變得非常流行,並且(除了迭代器之外)可以說是該書中最持久的模式之一。

Builder模式也可以被視為一種柯里化(currying)的方式,即將一個接受多個引數的函式轉換為一系列接受一個引數的函式。

為什麼使用Builder模式?

在Rust中,我們通常不希望直接暴露結構體,而且如前所述,Rust不支援可選引數。因此,與其依賴具有大量引數的建構子,我們可以使用Builder來處理更複雜的情況。

5.3.1 實作Builder模式

讓我們為我們想要建模的腳踏車寫一個基本的Builder。我們將建模圖5.1所示的關係。

// 定義Bicycle結構體和它的Builder
struct Bicycle {
    brand: String,
    size: u32,
    gear: u8,
}

struct BicycleBuilder {
    brand: Option<String>,
    size: Option<u32>,
    gear: Option<u8>,
}

impl BicycleBuilder {
    fn new() -> Self {
        BicycleBuilder {
            brand: None,
            size: None,
            gear: None,
        }
    }

    fn brand(mut self, brand: String) -> Self {
        self.brand = Some(brand);
        self
    }

    fn size(mut self, size: u32) -> Self {
        self.size = Some(size);
        self
    }

    fn gear(mut self, gear: u8) -> Self {
        self.gear = Some(gear);
        self
    }

    fn build(self) -> Result<Bicycle, &'static str> {
        match (self.brand, self.size, self.gear) {
            (Some(brand), Some(size), Some(gear)) => Ok(Bicycle { brand, size, gear }),
            _ => Err("Missing required fields"),
        }
    }
}

內容解密:

這段程式碼定義了一個Bicycle結構體和一個BicycleBuilder結構體。BicycleBuilder用於逐步構建Bicycle例項。它的方法(如brandsizegear)傳回Builder本身,使得可以鏈式呼叫。最後,build方法檢查所有必要欄位是否已設定,如果是,則傳回一個Bicycle例項;否則,傳回一個錯誤訊息。

fn main() {
    let bicycle = BicycleBuilder::new()
        .brand("Trek".to_string())
        .size(52)
        .gear(21)
        .build();

    match bicycle {
        Ok(b) => println!("Built bicycle: {:?}, size: {:?}, gear: {:?}", b.brand, b.size, b.gear),
        Err(e) => println!("Error building bicycle: {}", e),
    }
}

內容解密:

main 函式中,我們使用 BicycleBuilder 建立了一個 Bicycle 例項,並列印出結果。如果所有必要欄位都已正確設定,則會成功建立並列印腳踏車的詳細資訊;否則,會列印錯誤訊息。

建造者模式:超越基礎

建造者模式簡介

建造者模式是一種建立型設計模式,旨在將複雜物件的建構過程與其表示分離,從而使得相同的建構過程可以建立不同的表示。在本章中,我們將探討建造者模式在 Rust 程式語言中的實作。

基本實作

首先,我們定義一個 Bicycle 結構體來表示一輛腳踏車:

#[derive(Debug)]
pub struct Bicycle {
    make: String,
    model: String,
    size: i32,
    color: String,
}

impl Bicycle {
    accessor!(make, &String);
    accessor!(model, &String);
    accessor!(size, i32);
    accessor!(color, &String);
}

內容解密:

  • Bicycle 結構體包含四個屬性:makemodelsizecolor,分別代表腳踏車的品牌、型號、尺寸和顏色。
  • accessor! 巨集用於為 Bicycle 的屬性生成 getter 方法。根據屬性的型別不同,accessor! 巨集會生成傳回參照或複製的方法。

接下來,我們實作 BicycleBuilder 結構體及其方法:

pub struct BicycleBuilder {
    bicycle: Bicycle,
}

impl BicycleBuilder {
    pub fn new() -> Self {
        Self {
            bicycle: Bicycle {
                make: String::new(),
                model: String::new(),
                size: 0,
                color: String::new(),
            },
        }
    }

    with_str!(make, with_make);
    with_str!(model, with_model);
    with!(size, with_size, i32);
    with_str!(color, with_color);

    pub fn build(self) -> Bicycle {
        self.bicycle
    }
}

內容解密:

  • BicycleBuilder 結構體包含一個 Bicycle 例項,用於在建構過程中暫存腳踏車的屬性。
  • new 方法初始化一個新的 BicycleBuilder 例項,並將 Bicycle 的屬性設為預設值。
  • with_str!with! 巨集用於為 BicycleBuilder 生成設定屬性的方法。這些方法允許我們以鏈式呼叫的方式設定腳踏車的屬性。
  • build 方法傳回建構好的 Bicycle 例項。

使用建造者模式

現在,我們可以使用 BicycleBuilder 來建立 Bicycle 例項:

fn main() {
    let mut bicycle_builder = BicycleBuilder::new();
    bicycle_builder.with_make("Huffy");
    bicycle_builder.with_model("Radio");
    bicycle_builder.with_size(46);
    bicycle_builder.with_color("red");
    let bicycle = bicycle_builder.build();
    println!("My new bike: {:#?}", bicycle);
}

內容解密:

  • 我們首先建立一個新的 BicycleBuilder 例項。
  • 然後,我們使用 with_makewith_modelwith_sizewith_color 方法設定腳踏車的屬性。
  • 最後,我們呼叫 build 方法來取得建構好的 Bicycle 例項,並列印出來。

擴充套件建造者模式

為了使建造者模式更具通用性,我們可以定義一個 Builder 特徵:

pub trait Builder<T> {
    fn new() -> Self;
    fn build(self) -> T;
}

並為 BicycleBuilder 實作這個特徵:

impl Builder<Bicycle> for BicycleBuilder {
    fn new() -> Self {
        // ...
    }

    fn build(self) -> Bicycle {
        // ...
    }
}

同時,我們也可以定義一個 Buildable 特徵,用於提供取得建造者例項的方法:

pub trait Buildable<Target, B: Builder<Target>> {
    fn builder() -> B;
}

impl Buildable<Bicycle, BicycleBuilder> for Bicycle {
    fn builder() -> BicycleBuilder {
        BicycleBuilder::new()
    }
}

這樣,我們就可以透過 Bicycle::builder() 方法直接取得一個新的 BicycleBuilder 例項。

在未來,我們可以進一步探索建造者模式在其他領域的應用,例如在圖形介面程式設計或遊戲開發中。同時,我們也可以研究如何將建造者模式與其他設計模式結合使用,以解決更複雜的問題。

Mermaid 圖表說明

  classDiagram
    class Bicycle {
        -String make
        -String model
        -Integer size
        -String color
        +make() : String
        +model() : String
        +size() : Integer
        +color() : String
    }

    class BicycleBuilder {
        -Bicycle bicycle
        +with_make(String)
        +with_model(String)
        +with_size(Integer)
        +with_color(String)
        +build() : Bicycle
    }

    BicycleBuilder --> Bicycle : builds

圖表翻譯: 此圖表展示了 BicycleBicycleBuilder 之間的關係。BicycleBuilder 負責建立 Bicycle 例項,並提供了多個方法來設定腳踏車的屬性。最終,build 方法傳回建構好的 Bicycle 例項。