Option 不只是個選項
各位過去在寫程式的時候,有沒有遇過執行某些函數照理應該要回傳陣列,然後你會在這個陣列上呼叫 .map
或 .forEach
方法做點事情,但結果你拿到的不是陣列,而是一個 undefined
,然後程式就出錯了...
我用 JavaScript 舉個例子:
function getFriends() {
// 回傳朋友清單
}
const friends = getFriends() // 執行之後才發現自己沒朋友
friends.map(() => { ... }) // 出錯
遇到這種情況你會怎麼解決?通常是檢查 friends
是不是有東西,如果有的話才往下做:
if (friends) {
friends.map(() => { ... })
}
或是也可用短一點的 Optional Chaining 的寫法:
friends?.map(() => { ... })
這在 JavaScript 應該是很常見的做法,但大家看到現在,有沒有發現 Rust 並沒有 undefined
或 Null
或 nil
的空值的型別?並不是 Rust 不需要空值的設計,而是用了其它的方式來處理、判斷,第一個要介紹的就是 Option
。
Option
Option
翻譯成中文是「選項」,它是 Rust 內建的值,但如果大家去翻一下 Option
的原始碼,就會發現 Option
其實就只是一個我們在上個章節介紹的 Enum 而已(透過 VSCode 可以很容易就翻到 Rust 的原始碼),在這個 Enum 裡有 None
跟 Some
這兩個變體(Variant),其中 Some
這個變體還能帶參數:
pub enum Option<T> {
None,
Some(T),
}
上面這個寫法現在看起來應該不陌生了。其中變體 None
用來表示值不存在,變體 Some(T)
則是表示這個值是存在的,而且這個存在的值型別就是 T
。那個 T
請暫時先忽略它,我們會在下個章節介紹「泛型」的時候會再詳述。
所以就 Enum 本身來說,Option
並沒什麼特別的。前面提到 Rust 並沒有 Null
或 undefined
的設計,取而代之的是 None
,也就是 Option
這個 Enum 裡的 None
。
你有朋友嗎?
假設我寫了一個可以回傳朋友名單的 get_friends()
函數:
fn get_friends() {
// ...
}
大家先不管我這朋友的名單怎麼來的,get_friends()
這個函數所回傳結果可能是一個 Vector,所以我可以把這個函數的回傳型別設定成 Vec<u8>
,就算沒有朋友也給我 個空的 Vector 就好。但假設因為某些不確定的原因,它回傳的結果連 Vector 都不是的話怎麼辦?如果你知道這個函數有可能回傳空的值,你現在也知道 Rust 編譯器很龜毛,什麼事都要說清楚講明白,那麼你覺得 get_friends()
這個函數的回傳值型別該怎麼寫?這時候就可以拿 Option
這個 Enum 出來用:
fn get_friends() -> Option<Vec<u8>> {
// 可能回傳 Vec<u8>,也可能沒有回傳值
}
Option<Vec<u8>>
看起來有點複雜,它的意思告訴 Rust 編譯器說這個函數可能會有回傳值,也有可能不會有,但如果有的話,它會是一個 Vec<u8>
型別的值。雖然 Rust 不喜歡不確定性,但至少你把這種不確定性直白的跟它說,減少一點它的不安,Rust 的編譯器還是可以接受的。
這樣函數裡面該怎麼寫?我稍微改了一下原本的函數簽名,這樣看起來比較容易說明:
fn get_friends(has_money: bool) -> Option<Vec<u8>> {
if !has_money {
return None;
}
let friends: Vec<u8> = vec![1, 2, 3, 4, 5];
Some(friends)
}
我多傳了一個 has_money
參數來做判斷,如果沒有錢錢就沒有朋友(好現實),所以就回傳個 Option
裡面的 None
變體回來,反正如果有錢有朋友的話就會回傳另一個變體 Some(T)
回來,並且把朋友名單包在變體裡。
上面這個情境還是比較可以控制的,至少它跟傳入的參數有關,但說不定有更不可控或是跟系統或環境變數設定有關,你不一定能保證最後得到什麼結果。看到這裡你也許會想「如果沒東西,那就回傳一個空陣列就好啦,為什麼還要刻意回傳一個 None
回來?」
是的,你的想法是正確的,沒結果的時候回傳空陣列是一種做法,你在 Rust 也可以這樣做沒問題:
fn get_friends(has_money: bool) -> Vec<u8> {
if !has_money {
return vec![];
}
let friends: Vec<u8> = vec![1, 2, 3, 4, 5];
return friends;
}
沒錢錢就回傳一個空的 Vector 回來就好,然後在判斷的時候只要判斷 Vector 裡有沒有元素就知道有沒有朋友了:
let friends = get_friends(false);
if friends.len() == 0 {
println!("我是邊緣人我驕傲!")
} else {
println!("我有好多朋友 {:?}", friends)
}
一般程式很常看到這樣的寫法。但如果利用回傳 Option
型別再搭配在上個章節介紹過的 match
,可以讓流程變的更清楚一點:
let friends = get_friends(false);
match friends {
None => println!("我是邊緣人我驕傲!"),
Some(list) => println!("我有好多朋友 {:?}", list)
}
透過 Pattern Matching,如果比對到 Some(T)
變體,剛才回傳的時候包在 Some(T)
變體的東西,就可以在現在拿出來用了。
這樣是不是流程看起來更清楚了?這樣的寫法在 Rust 裡還滿見的。
打開包裝盒
Option
除了搭配 match
之外,也能直接拿來用:
let friends = get_friends(true);
println!("{:?}", friends);
直接印的話,你並不會印出真正的朋友名單,而是印出 Some([1, 2, 3, 4, 5])
這個變體。你想要的資料被 Some(T)
變體包著,如果想要取得這個變體裡的內容的話,可以使用 .unwrap()
方法把它「打開」:
println!("{:?}", friends.unwrap());
透過 .unwrap()
方法就可以把變體 Some(T)
裡的東西拿出來,但萬一你拿到的是 None
變體的話,對它做 .unwrap()
會得到錯誤訊息,所以要小心使用,確定 Option
有值再用它,或是就乾脆用 match
就好。
如果大家有興趣想知道 .unwrap()
實際上是怎麼運作的,翻一下原始碼就會發現它是這樣定義的:
pub const fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic("called `Option::unwrap()` on a `None` value"),
}
}