如果你用过其他编程语言(C/C++、Python、Java、Ruby、Lisp 等),那么你很可能遇到过null或nil。
这些语言用null或nil来表示某个变量没有值。
但这会让“不小心使用了指向null的变量”成为一个很容易犯(而且很常见)的错误。
可以想见,试图调用一个并不存在的函数,或者访问一个并不存在的值,都会引发各种各样的 bug 和崩溃。
null的创造者甚至把它称作自己的“十亿美元错误”。
Option类型为了避免这些问题,Rust 没有使用空引用。
不过,它仍然需要一种安全的方式来表示某个变量没有值。
这正是Option的用武之地。
与其让变量缺少值,Rust 变量可以使用Option枚举。
这个枚举有两个变体:None,相当于 Rust 里的 null;以及Some(T),其中 T 是任意类型的值。
它看起来是这样的:
enum Option<T> {
None,
Some(T),
}
你可以把Option看作一层安全屏障,隔在你和空引用可能引发的问题之间,同时保留了它们在概念上的用处。
Option
把变量设为None相当直接:
let nothing: Option<u32> = None; // Variable nothing is set to None
不过,如果你想让Option类型携带一个值,就不能直接把这个值赋给它。
Option类型的变量和(比方说)i32类型的变量并不等价。
你需要使用Some:
let wrong_way: Option<i32> = -4; // This will not work
let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>
也正是出于这个原因,下面这段代码也不行:
let number = 47;
let option_number = Some(15);
let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types
如果你想取出Some中包含的值,首先需要确认它确实存在:
let mut some_words = Some("choose something to say"); // some_words set to something
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"
some_words = None; // some_words now set to None
// exactly the same match block as above
match some_words {
Some(str) => println!("Here, we will {}", str),
None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"
除了match,Rust 还提供了其他工具来检查和访问Option中包含的值,不过你现在应该已经很熟悉match了。
另外,你也可以把这看作一个示范,说明 Rust 为什么用Option而不是空引用。
重点在于,你_必须_检查Option变量究竟是Some(如果是,你就可以继续取出并使用其中包含的值),还是None。
除此之外的任何情况,你的程序都无法通过编译;编译器会保护你免受null的影响。
你正在实现一个角色扮演游戏。玩家的角色由下面的结构表示:
pub struct Player {
health: u32,
mana: Option<u32>,
level: u32,
}
这个游戏里的玩家必须先升到 10 级,才能解锁法力池,进而开始施放法术。在那之前,玩家(Player)的法力值为 None。
你要实现这个游戏里的两项功能:复活机制和施法机制。
revive方法应当先检查 Player 是否确实已经死亡(生命值已经降到 0)。如果确实如此,就返回一个生命值为 100 的新 Player 实例。
如果 Player 的等级在 10 级或以上,复活时还应带 100 法力值。
如果 Player 的等级低于 10 级,其法力值应为 None。revive方法应当保留 Player 的等级。
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
如果对一个生命值为 1 或以上的 Player 调用revive方法,那么该方法应当返回 None。
let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None
cast_spell方法接受 Player 的可变引用,以及一个 mana_cost 形参,用来表示这个法术消耗多少法力。它返回所施放法术造成的伤害量。如果法术成功施放,伤害量始终是法术法力消耗的两倍。
如果玩家无法力池可用,尝试施放法术会使其生命值减少该法术的法力消耗。返回的伤害必须是 0。
let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
assert_eq!(not_a_wizard_yet.health, 74);
assert_eq!(not_a_wizard_yet.mana, None);
如果玩家有法力池但法力不足,该方法不应影响法力池,而应返回 0
let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
assert_eq!(low_mana_wizard.cast_spell(10), 0);
assert_eq!(low_mana_wizard.health, 93);
assert_eq!(low_mana_wizard.mana, Some(3));
否则,应从 Player 的法力池中扣除 mana_cost,并返回相应的伤害量。
let wizard = Player { health: 123, mana: Some(30), level: 18 };
assert_eq!(wizard.cast_spell(10), 20);
assert_eq!(wizard.health, 123);
assert_eq!(wizard.mana, Some(20));
祝你玩得开心!