如果你曾使用過其他程式語言(C/C++、Python、Java、Ruby、Lisp 等),那麼你很可能遇過null或nil。
使用null或nil,是這些語言用來表示某個變數沒有值的方式。
然而,這也讓不小心使用到指向null的變數,成為一個容易(而且經常)犯下的錯誤。
可以想見,試圖呼叫一個不存在的函式,或存取一個不存在的值,可能導致各種 bug 和當機。
null的發明者甚至將它稱為他的「十億美元錯誤」。
Option型別為了避免這些問題,Rust 不使用空參照。
然而,它仍然需要一種安全的方式,來表示某個變數沒有值。
這就是Option派上用場的地方。
與其讓變數缺少值,Rust 的變數可以使用Option列舉。
這個列舉有兩個變體:None,等同於 Rust 的空值;以及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 才能解鎖魔力池,開始施放法術。在那之前,玩家的魔力是None。
你正在實作這款遊戲裡的兩項功能:復活機制和施放法術機制。
revive方法應該檢查玩家是否確實已死亡(生命值降到 0),如果是,就回傳一個生命值為 100 的新 Player 實例。
如果玩家的等級為 10 或以上,復活時也應該有 100 魔力。
如果玩家的等級低於 10,魔力應該是None。revive方法應該保留玩家的等級。
let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }
如果對生命值為 1 或以上的玩家呼叫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));
否則,應該從玩家的魔力池扣除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));
玩得開心!