Default 和生成器模式

你可以實作 Default 特徵在你認為最常見的 structenum 上用來賦值。生成器模式可以很好地和它配合,來讓使用者在需要時輕鬆地進行修改。首先我們來看看 Default。實際上,在 Rust 大多數的通用型別已經有 Default。它們並不另人意外:像是 0、""(空字串)、false, 等等。

fn main() {
    let default_i8: i8 = Default::default();
    let default_str: String = Default::default();
    let default_bool: bool = Default::default();

    println!("'{}', '{}', '{}'", default_i8, default_str, default_bool);
}

印出 '0', '', 'false'

所以 Default 就像 new 函式一樣,除了你不需要輸入任何東西。首先我們將要建立還沒有實現 Defaultstruct。它有個 new 函式是我們用來做出名為比利 (Billy) 的角色並附帶一些角色個人資訊。

struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive { LifeState::Alive } else { LifeState::Dead },
        }
    }
}

fn main() {
    let character_1 = Character::new("Billy".to_string(), 15, 170, 70, true);
}

但也許在我們的世界裡,我們希望大部分角色都叫比利,年齡 15 歲、身高 170、體重 70,還活著。我們可以實作 Default,這樣我們就可以只寫 Character::default()。它看起來像這樣:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive {
                LifeState::Alive
            } else {
                LifeState::Dead
            },
        }
    }
}

impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

fn main() {
    let character_1 = Character::default();

    println!(
        "The character {:?} is {:?} years old.",
        character_1.name, character_1.age
    );
}

印出 The character "Billy" is 15 years old. 簡單多了!

現在我們來看生成器模式。我們會有很多比利,所以我們會保留預設的。但是很多其他角色只會有一點點不同。生成器模式讓我們可以把小方法連結起來,每次改變一個值。在 Character 裡這就是一個這樣的方法:

#![allow(unused)]
fn main() {
fn height(mut self, height: u32) -> Self {    // 🚧
    self.height = height;
    self
}
}

一定要注意,它接受的是 mut self。我們之前看到過一次,它不是可變引用(&mut self)。它取得了 Self 的所有權,並且有了 mut,它將是可變的,即使它先前不是可變的。這是因為 .height() 擁有完整的所有權,沒人能接觸它,所以它能安全的作為可變變數來用。接著它只是改變 self.height,並回傳 Self(也就是 Character)。

所以我們有三個這樣的生成器方法。它們幾乎是一樣的:

#![allow(unused)]
fn main() {
fn height(mut self, height: u32) -> Self {     // 🚧
    self.height = height;
    self
}

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

fn name(mut self, name: &str) -> Self {
    self.name = name.to_string();
    self
}
}

這些每一個都會改變其中一個變數,並給出 Self 回傳:這就是你在生成器模式中會看到的。所以現在我們類似這樣寫些東西來做出角色:let character_1 = Character::default().height(180).weight(60).name("Bobby");。如果你正在建造函式庫給別人使用,這可以讓他們很容易使用。對終端使用者來說很容易,因為它看起來幾乎像是自然的英語:"給我預設的角色,但身高為 180、體重為 60、名字是 Bobby。" 到目前為止,我們的程式碼看起來像這樣:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) -> Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive {
                LifeState::Alive
            } else {
                LifeState::Dead
            },
        }
    }

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

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

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
}

impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

fn main() {
    let character_1 = Character::default().height(180).weight(60).name("Bobby");

    println!("{:?}", character_1);
}

最後一個要新增的方法通常叫 .build()。這個方法是某種最終檢查。當你給使用者提供像是 .height() 這樣的方法時,你可以確保他們只輸入 u32(),但是如果他們輸入身高為 5000 時怎麼辦?在你正在製作的遊戲中那可能就不對了。我們將使用名為 .build() 的最後方法去回傳 Result。在它裡面我們將檢查使用者輸入是否正常,如果正常的話我們將回傳 Ok(Self)

不過首先讓我們更改 .new() 方法。我們不希望使用者再自由建立任何一種角色。所以我們將把 impl Default 的值移到 .new()。而現在 .new() 不再接受任何輸入。

#![allow(unused)]
fn main() {
    fn new() -> Self {    // 🚧
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}

這意味著我們不再需要 impl Default 了,因為 .new() 有所有的預設值。所以我們可以刪除 impl Default

現在我們的程式碼像這樣:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }

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

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

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
}

fn main() {
    let character_1 = Character::new().height(180).weight(60).name("Bobby");

    println!("{:?}", character_1);
}

印出來的結果一樣:Character { name: "Bobby", age: 15, height: 180, weight: 60, lifestate: Alive }

我們幾乎已經準備好寫 .build() 方法了,但是還有個問題:要如何讓使用者使用它?現在使用者可以寫 let x = Character::new().height(76767);,然後得到 Character。有很多方式可以做到這一點,也許你能想出自己的方法。但是我們會在 Character 中加上 can_use: bool 的值。

#![allow(unused)]
fn main() {
#[derive(Debug)]       // 🚧
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool, // 設定使用者是否能使用角色
}

// Cut other code

    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true, // .new() 永遠給出好的角色, 所以是 true
        }
    }
}

而對於其他的方法,比如 .height(),我們會將 can_use 設定為 false。只有 .build() 會再次設定為 true,所以現在使用者要用 .build() 做最後的檢查。我們要確保 height 不高於 200,weight 不寬於 300。另外,在我們的遊戲中,有個不好的字叫 smurf,我們不希望任何角色使用它。

我們的 .build() 方法像這樣:

#![allow(unused)]
fn main() {
fn build(mut self) -> Result<Character, String> {      // 🚧
    if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") {
        self.can_use = true;
        Ok(self)
    } else {
        Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
            .to_string())
    }
}
}

!self.name.to_lowercase().contains("smurf") 確保使用者不會寫出類似 "SMURF" 或 "IamSmurf" 的字樣。它讓整個 String 都變成小寫字母,並檢查 .contains() 而不是 ==。而前面的 ! 表示"不是"(邏輯運算補數)。

如果一切正常,我們就把 can_use 設定為 true,然後把角色包在 Ok 裡面回傳給使用者。

現在我們的程式碼已經完成了,我們將建立三個不能使用的角色,及一個能使用的角色。最後的程式碼像這樣:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool, // 這裡是新的值
}

#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}

impl Character {
    fn new() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true,  // .new() 做出可用的角色, 所以是 true
        }
    }

    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self.can_use = false; // 現在使用者還不能使用角色
        self
    }

    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self.can_use = false;
        self
    }

    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self.can_use = false;
        self
    }

    fn build(mut self) -> Result<Character, String> {
        if self.height < 200 && self.weight < 300 && !self.name.to_lowercase().contains("smurf") {
            self.can_use = true;   // 一切都沒問題, 所以設定為 true
            Ok(self)               // 並回傳角色
        } else {
            Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
                .to_string())
        }
    }
}

fn main() {
    let character_with_smurf = Character::new().name("Lol I am Smurf!!").build(); // 這一個包含 "smurf" - 不行
    let character_too_tall = Character::new().height(400).build(); // 太高 - 不行
    let character_too_heavy = Character::new().weight(500).build(); // 太重 - 不行
    let okay_character = Character::new()
        .name("Billybrobby")
        .height(180)
        .weight(100)
        .build();   // 這個角色沒問題. 名字很好、身高體重也都很好

    // 現在它們還不是 Character, 它們是 Result<Character, String>. 所以讓我們把它們放進 Vec 裡,那樣我們就能一起處理它們:
    let character_vec = vec![character_with_smurf, character_too_tall, character_too_heavy, okay_character];

    for character in character_vec { // 現在我們會印出角色如果是 Ok, 以及印出錯誤如果是 Err
        match character {
            Ok(character_info) => println!("{:?}", character_info),
            Err(err_info) => println!("{}", err_info),
        }
        println!(); // 再多加上一個換行
    }
}

將會印出:

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)

Character { name: "Billybrobby", age: 15, height: 180, weight: 100, lifestate: Alive, can_use: true }