默认值和建造者模式
你可以实现 Default
trait,给你认为最常见的 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
函数一样,除了你不需要输入任何东西。首先我们要创建一个struct
,它还没有实现Default
。它有一个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.
,简单多了!
现在我们来看建造者模式。我们会有很多Billy,所以我们会保留默认的。但是很多其他角色只会有一点不同。建造者模式让我们可以把小方法链接起来,每次改变一个值。这里是一个Character
的方法:
一定要注意,它取的是mut self
。我们之前看到过一次,它不是一个可变引用(&mut self
)。它占用了Self
的所有权,有了mut
,它将是可变的,即使它之前不是可变的。这是因为.height()
拥有完全的所有权,别人不能碰它,所以它是安全的,可变。它只是改变self.height
,然后返回Self
(就是Character
)。
所以我们有三个这样的构建方法。它们几乎是一样的:
每一个都会改变一个变量,并回馈给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()
不接受任何输入。
这意味着我们不再需要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
的值。
而对于其他的方法,比如.height()
,我们会将can_use
设置为false
。只有.build()
会再次设置为true
,所以现在用户要用.build()
做最后的检查。我们要确保height
不高于200,weight
不高于300。另外,在我们的游戏中,有一个不好的字叫smurf
,我们不希望任何角色使用它。
我们的.build()
方法是这样的:
!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, // Here is the new value
}
#[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() makes a fine character, so it is true
}
}
fn height(mut self, height: u32) -> Self {
self.height = height;
self.can_use = false; // Now the user can't use the character
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; // Everything is okay, so set to true
Ok(self) // and return the character
} 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(); // This one contains "smurf" - not okay
let character_too_tall = Character::new().height(400).build(); // Too tall - not okay
let character_too_heavy = Character::new().weight(500).build(); // Too heavy - not okay
let okay_character = Character::new()
.name("Billybrobby")
.height(180)
.weight(100)
.build(); // This character is okay. Name is fine, height and weight are fine
// Now they are not Character, they are Result<Character, String>. So let's put them in a Vec so we can see them:
let character_vec = vec![character_with_smurf, character_too_tall, character_too_heavy, okay_character];
for character in character_vec { // Now we will print the character if it's Ok, and print the error if it's Err
match character {
Ok(character_info) => println!("{:?}", character_info),
Err(err_info) => println!("{}", err_info),
}
println!(); // Then add one more line
}
}
这将打印:
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 }