问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Option 不只是一个选项

创作时间:
作者:
@小白创作中心

Option 不只是一个选项

引用
1
来源
1.
https://rustbook.cc/chapters/option-and-result

Option 不只是一个选项

各位过去在写程序的时候,有没有遇到执行某些函数照理应该要返回数组,然后你会在这个数组上调用 .map.forEach 方法做点事情,但结果你拿到的不是一个数组,而是一个 undefined,然后程序就出错了……

我用 JavaScript 举个例子:

function getFriends() {
  // 返回朋友清单
}
const friends = getFriends();  // 执行之后才发现自己没朋友
friends.map(() => { ... });    // 出错

遇到这种情况你会怎么解决?通常是检查 friends 是不是有东西,如果有的话才往下做:

if (friends) {
  friends.map(() => { ... });
}

或是也可用短一点的Optional Chaining的写法:

friends?.map(() => { ... });

这在 JavaScript 应该是很常见做法,但大家看到现在,有没有发现 Rust 并没有 undefinednullnil 的空值的类型?并不是 Rust 不需要空值的设计,而是用了其它的方式来处理、判断,第一个要介绍的就是 Option

Option

Option 翻译成中文是“选项”,它是 Rust 内建的值,但如果大家去翻一下 Option 的源码,就会发现 Option 其实就只一个我们在上个章节介绍的 Enum 而已(透过 VSCode 可以很容易就翻到 Rust 的源码),在这个 Enum 里有 NoneSome 这两个变体(Variant),其中 Some 这个变体还能带参数:

enum Option<T> {
    None,
    Some(T),
}

上面这个写法现在看起来应该不陌生了。其中变体 None 用来表示值不存在,变体 Some(T) 则是表示这个值是存在的,而且这个存在的值类型就是 T。那个 T 请暂时先忽略它,我们会在下个章节介绍“泛型”的时候再详述。

所以就 Enum 本身来说,Option 并没有什么特别的。前面提到 Rust 并没有 nullundefined 的设计,取而代之的是 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,
    }
}
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号