Default 和生成器模式
你可以實作 Default
特徵在你認為最常見的 struct
或 enum
上用來賦值。生成器模式可以很好地和它配合,來讓使用者在需要時輕鬆地進行修改。首先我們來看看 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
函式一樣,除了你不需要輸入任何東西。首先我們將要建立還沒有實現 Default
的 struct
。它有個 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 }