Option 不只是一个选项
Option 不只是一个选项
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
这个变体还能带参数:
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() -> Vec<u8> {
// ...
}
大家先不管我这朋友的名单怎么来的,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.is_empty() {
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 {
None => panic("called `Option::unwrap()` on a `None` value"),
Some(t) => t,
}
}