Rust 的型別系統嚴謹,能有效避免許多常見的程式錯誤。然而,在處理不同單位或語義的資料時,例如物理量的轉換,單純依靠型別系統並不足以確保程式碼的正確性。新型別模式提供了一個優雅的解決方案,它允許我們為相同底層資料型別賦予不同的語義,從而提升程式碼的可讀性和安全性。例如,可以利用新型別模式包裝 f64,分別表示牛頓秒和磅力秒,避免單位混淆造成的錯誤。此外,本文也介紹了建構器模式,它可以簡化複雜資料結構的初始化過程,提升程式碼的可維護性,尤其在結構體欄位較多且部分欄位可選的情況下,建構器模式更能展現其優勢。兩種模式各有千秋,選擇哪種模式取決於具體的應用場景和需求。

瞭解型別轉換和新型別模式

在 Rust 中,型別轉換是一個重要的概念,尤其是在處理不同單位或語義的資料時。型別轉換可以分為兩種:強制轉換(coercion)和顯式轉換(explicit conversion)。本文將介紹 Rust 中的型別轉換,特別是新型別模式(newtype pattern),以及如何使用它們來提高程式碼的安全性和可讀性。

型別轉換

Rust 中的型別轉換可以分為兩種:強制轉換和顯式轉換。強制轉換是指編譯器自動進行的型別轉換,例如將 i32 轉換為 i64。顯式轉換則需要程式員明確地進行轉換,例如使用 as 關鍵字將 i32 轉換為 i64

新型別模式

新型別模式是一種使用 tuple struct 來建立新型別的方法。tuple struct 是一種只有單一欄位的 struct,通常用於建立新型別。新型別模式的優點是可以增加程式碼的安全性和可讀性,特別是在處理不同單位或語義的資料時。

例如,假設我們要處理火箭發動機的推力和慣性導航系統的力矩,我們可以定義兩個新型別:PoundForceSecondsNewtonSeconds

pub struct PoundForceSeconds(pub f64);
pub struct NewtonSeconds(pub f64);

然後,我們可以使用這兩個新型別來定義火箭發動機和慣性導航系統的函式:

pub fn thruster_impulse(direction: Direction) -> PoundForceSeconds {
    //...
    return PoundForceSeconds(42.0);
}

pub fn update_trajectory(force: NewtonSeconds) {
    //...
}

這樣,我們就可以確保火箭發動機的推力和慣性導航系統的力矩是正確的單位,並且避免了誤用不同單位的資料。

內容解密:
  • Rust 中的型別轉換可以分為強制轉換和顯式轉換。
  • 新型別模式是一種使用 tuple struct 來建立新型別的方法。
  • 新型別模式可以增加程式碼的安全性和可讀性,特別是在處理不同單位或語義的資料時。
  • 透過使用新型別模式,我們可以建立新型別並增加程式碼的安全性和可讀性。

圖表翻譯:

  flowchart TD
    A[定義新型別] --> B[使用新型別定義函式]
    B --> C[增加程式碼安全性和可讀性]
    C --> D[避免誤用不同單位的資料]

這個流程圖展示瞭如何使用新型別模式來增加程式碼的安全性和可讀性。首先,我們定義新型別,然後使用新型別定義函式,最後增加程式碼的安全性和可讀性,並避免誤用不同單位的資料。

使用Newtype模式解決Rust中的單位與型別轉換問題

在Rust中,使用Newtype模式可以幫助解決單位與型別轉換的問題。Newtype是一種包裝現有型別的方法,可以為其新增額外的語義資訊。

問題描述

當我們需要將一個單位為PoundForceSeconds的值轉換為NewtonSeconds時,直接傳遞給函式會出現錯誤,因為函式預期的引數型別是NewtonSeconds

解決方案

我們可以定義一個Newtype結構體NewtonSeconds,並實作From特徵,以便將PoundForceSeconds轉換為NewtonSeconds

struct NewtonSeconds(f64);
struct PoundForceSeconds(f64);

impl From<PoundForceSeconds> for NewtonSeconds {
    fn from(val: PoundForceSeconds) -> NewtonSeconds {
        NewtonSeconds(4.448222 * val.0)
    }
}

然後,我們就可以使用into()方法將PoundForceSeconds轉換為NewtonSeconds,並傳遞給函式。

let thruster_force: PoundForceSeconds = thruster_impulse(direction);
let new_direction = update_trajectory(thruster_force.into());

其他應用場景

Newtype模式也可以用於解決其他問題,例如使布林值引數更明確。

struct DoubleSided(pub bool);
struct ColorOutput(pub bool);

fn print_page(sides: DoubleSided, color: ColorOutput) {
    //...
}

print_page(DoubleSided(true), ColorOutput(false));

如果需要考慮大小效率或二進位制相容性,可以使用#[repr(transparent)]屬性。

#[repr(transparent)]
struct NewtonSeconds(f64);

繞過孤兒規則

Newtype模式也可以用於繞過孤兒規則(Orphan Rule),即當我們需要實作一個外部特徵(trait)為外部型別時,可以定義一個Newtype結構體,並實作該特徵。

struct MyStdRng(rand::rngs::StdRng);

impl std::fmt::Display for MyStdRng {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
        write!(f, "<MyStdRng instance>")
    }
}

這樣,我們就可以實作外部特徵為外部型別,而不會違反孤兒規則。

Rust 中的新型別模式和建構器模式

在 Rust 中,新型別模式(newtype pattern)是一種常見的設計模式,用於解決特定問題。它的基本思想是建立一個新的型別,該型別包含另一個型別的例項。這種模式可以幫助我們避免單位轉換和繞過孤兒規則(orphan rule)。

新型別模式

新型別模式的主要優點是可以防止單位轉換和繞過孤兒規則。例如,如果我們想要實作 fmt::Display 特徵 для rand::rngs::StdRng 型別,但由於孤兒規則而無法直接實作,那麼我們可以建立一個新的型別,例如 MyRng,並包含 StdRng 的例項。

struct MyRng(rand::rngs::StdRng);

impl fmt::Display for MyRng {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        write!(f, "<MyRng instance>")
    }
}

然而,新型別模式也有一些限制。例如,每次操作新型別時,都需要轉發到內部型別,這可能會導致一些不便。另外,內部型別的特徵實作也會丟失。

建構器模式

建構器模式(builder pattern)是一種用於建立複雜資料結構的設計模式。它的基本思想是建立一個與複雜資料結構相關的建構器型別,使用者更容易建立例項。

在 Rust 中,建構器模式可以用於避免建立複雜資料結構時的冗長程式碼。例如,如果我們有一個 Details 結構體,有多個欄位,其中一些欄位是可選的,那麼我們可以建立一個 DetailsBuilder 型別來幫助使用者建立 Details 的例項。

#[derive(Debug, Default)]
pub struct Details {
    pub given_name: String,
    pub preferred_name: Option<String>,
    pub middle_name: Option<String>,
    pub family_name: String,
    pub mobile_phone: Option<PhoneNumberE164>,
}

pub struct DetailsBuilder {
    given_name: String,
    preferred_name: Option<String>,
    middle_name: Option<String>,
    family_name: String,
    mobile_phone: Option<PhoneNumberE164>,
}

impl DetailsBuilder {
    pub fn new(given_name: String, family_name: String) -> Self {
        DetailsBuilder {
            given_name,
            family_name,
           ..Default::default()
        }
    }

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

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

    pub fn mobile_phone(mut self, mobile_phone: PhoneNumberE164) -> Self {
        self.mobile_phone = Some(mobile_phone);
        self
    }

    pub fn build(self) -> Details {
        Details {
            given_name: self.given_name,
            preferred_name: self.preferred_name,
            middle_name: self.middle_name,
            family_name: self.family_name,
            mobile_phone: self.mobile_phone,
        }
    }
}

使用建構器模式,可以更容易地建立 Details 的例項,並避免冗長的程式碼。

let dizzy = DetailsBuilder::new("Dizzy".to_owned(), "Mixer".to_owned())
   .preferred_name("Diz".to_owned())
   .middle_name("Mix".to_owned())
   .mobile_phone(PhoneNumberE164("1234567890".to_owned()))
   .build();

結構體初始化的最佳實踐

在 Rust 中,初始化結構體(struct)是一個常見的任務。然而,當結構體包含多個欄位時,手動初始化每個欄位可能會很繁瑣且容易出錯。這就是為什麼使用 Default 特徵(trait)和構建器(builder)模式來簡化結構體初始化的過程是非常重要的。

使用 Default 特徵

Default 特徵提供了一種方法,可以為結構體自動生成預設值。這對於那些具有多個欄位的結構體尤其有用,因為它可以減少手動初始化每個欄位所需的程式碼量。

#[derive(Debug, Default)]
pub struct Details {
    pub given_name: String,
    pub family_name: String,
    //...
}

let dizzy = Details {
    given_name: "Dizzy".to_owned(),
    family_name: "Mixer".to_owned(),
   ..Default::default()
};

在上面的例子中,Details 結構體實作了 Default 特徵,這使得我們可以使用 ..Default::default() 來初始化結構體的其餘欄位。

使用構建器模式

但是,如果結構體中有一些欄位不能實作 Default 特徵,例如 time::Date,那麼就不能使用 Default 特徵了。在這種情況下,構建器模式是一個很好的替代方案。

構建器模式涉及建立一個單獨的型別,該型別負責構建目標結構體。這個型別通常具有與目標結構體相同的欄位,但它們都是可選的。然後,可以使用這個型別來建立目標結構體,並提供必要的欄位值。

pub struct DetailsBuilder {
    given_name: Option<String>,
    family_name: Option<String>,
    //...
}

impl DetailsBuilder {
    pub fn new() -> Self {
        DetailsBuilder {
            given_name: None,
            family_name: None,
            //...
        }
    }

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

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

    //...
}

impl DetailsBuilder {
    pub fn build(self) -> Details {
        Details {
            given_name: self.given_name.unwrap(),
            family_name: self.family_name.unwrap(),
            //...
        }
    }
}

let bob = DetailsBuilder::new()
   .given_name("Robert".to_owned())
   .family_name("Builder".to_owned())
    //...
   .build();

在上面的例子中,DetailsBuilder 型別負責構建 Details 結構體。它提供了一系列方法,用於設定結構體的各個欄位。最後,可以使用 build 方法來建立最終的 Details 結構體。

圖表翻譯:

  classDiagram
    class Details {
        -given_name: String
        -family_name: String
        -date_of_birth: time::Date
        -last_seen: Option<time::OffsetDateTime>
    }

    class DetailsBuilder {
        -given_name: Option<String>
        -family_name: Option<String>
        -date_of_birth: Option<time::Date>
        -last_seen: Option<time::OffsetDateTime>

        +new() DetailsBuilder
        +given_name(given_name: String) DetailsBuilder
        +family_name(family_name: String) DetailsBuilder
        +build() Details
    }

    DetailsBuilder --* Details

內容解密:

在這個例子中,我們展示瞭如何使用 Default 特徵和構建器模式來簡化結構體初始化的過程。使用 Default 特徵可以自動為結構體生成預設值,而構建器模式則提供了一種更靈活的方式來初始化結構體。這兩種方法都可以幫助減少手動初始化結構體所需的程式碼量,並使程式碼更易於維護和擴充套件。

圖表翻譯:

上面的 Mermaid 圖表顯示了 Details 結構體和 DetailsBuilder 型別之間的關係。DetailsBuilder 型別負責構建 Details 結構體,並提供了一系列方法用於設定結構體的各個欄位。最後,可以使用 build 方法來建立最終的 Details 結構體。

建立者模式:複雜資料結構的建構

在處理複雜的資料結構時,使用建構者模式(Builder Pattern)可以大大提高程式碼的可讀性和維護性。建構者模式是一種設計模式,它允許您分步驟地建構複雜的物件,而不需要一次性地傳遞所有必要的引數。

從程式碼架構設計的視角來看,Rust 的新型別模式和建構器模式為開發者提供了強大的工具來管理複雜的資料結構和單位轉換。新型別模式巧妙地利用了 tuple struct,在不改變底層資料型別的前提下,賦予資料新的語義,從而提升程式碼的型別安全性和可讀性。配合 From trait 的轉換,更能有效地控制不同單位間的轉換邏輯,避免潛在的錯誤。建構器模式則著眼於簡化複雜資料結構的初始化過程,尤其在欄位眾多且部分為可選的情況下,更能體現其優勢。它允許開發者逐步設定欄位值,並最終建構出完整的物件,避免了繁瑣的建構式引數傳遞,也提升了程式碼的可維護性。然而,新型別模式在使用時需要注意內部型別特徵的轉發問題,而建構器模式則需考量建構器本身的設計複雜度。對於追求程式碼簡潔性和可維護性的專案,建議深入評估並採用這些模式。隨著 Rust 生態的持續發展,預計這些模式將在更多場景中得到應用,並衍生出更具彈性的變體,進一步提升 Rust 的開發效率和程式碼品質。玄貓認為,熟練掌握並運用新型別模式和建構器模式,將有助於開發者寫出更具 Rust 風格的高品質程式碼。