别再使用 `unwrap` 处理 Option 了:还有更好的方法 (2024)
别再使用 unwrap
处理 Options:还有更好的方法
发布时间:2024-08-29
我注意到在 Rust 中,处理 Option
的 None
变体而不依赖 unwrap()
是一种常见的痛点。更具体地说,当你想要从一个返回 Result
的函数中提前返回,如果遇到 None
时,问题就出现了。
这个问题已经被讨论过无数次了,但令人惊讶的是,即使是 Rust 官方书籍也没有提到我最喜欢的处理方法,而且许多论坛帖子已经过时。
经过一些练习,可靠地处理 None
可以变得像 unwrap()
一样简单,但更安全。
如果你时间紧迫,只需要一个快速的建议,可以直接跳到结尾。
问题所在
很常见的情况是,人们会编写如下代码:
// 假设这从某个地方获取用户
fn get_user() -> Option<String> {
None
}
fn get_user_name() -> Result<String, String> {
let user = get_user()?;
// 对 `user` 做一些处理
// ...
Ok(user)
}
这里的目标是在 Option
中遇到 None
时提前返回,所以他们使用 ?
操作符来传播错误。
唉,这段代码无法编译。相反,你会得到一个可怕的错误信息:
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/lib.rs:10:26
|
9 | fn get_user_name() -> Result<String, String> {
| ------------------------------------ this function returns a `Result`
10 | let user = get_user()?;
| ^ use `.ok_or(...)?` to provide an error compatible with `std::result::Result<String, Box<dyn std::error::Error>>`
|
= help: the trait `FromResidual<Option<Infallible>>` is not implemented for `std::result::Result<String, Box<dyn std::error::Error>>`
= help: the following other types implement trait `FromResidual<R>`:
<std::result::Result<T, F> as FromResidual<Yeet<E>>>
<std::result::Result<T, F> as FromResidual<std::result::Result<Infallible, E>>>
哇。这看起来很吓人!
这个错误信息中有很多视觉噪音。FromResidual
和 Yeet
是实现细节,可能会让新用户感到困惑,而且相关的细节有些模糊。
而我们所做的只是尝试将 ?
操作符用于我们的 Option
。
我对这个错误信息的主要不满是,它没有解释 为什么 ?
操作符在这种情况下不适用于 Option
… 只是说它不行。
人们最终会怎么做
我看到最常见的做法是:
- 人们会困惑一段时间。
- 他们试图理解错误信息。
- 最终,他们放弃并直接添加
unwrap()
。 - 他们在脑海中记下稍后再回来处理。
- “稍后”永远不会到来。
这是他们最终得到的:
fn get_user_name() -> Result<String, String> {
let user = get_user().unwrap();
// 对 `user` 做一些处理
Ok(user)
}
在培训中,我注意到人们常常不好意思寻求帮助。他们认为人们应该 “理解这一点”,而他们是唯一不理解的人。
这只是推迟了问题。函数的用户可能会在运行时遇到 panic
。该用户可能是他们未来的自己。
unwrap
在许多情况下都很好,但不应该是处理意外情况的第一个直觉。特别是当你编写一个库或一个函数,它是较大代码库的一部分时,你应该努力优雅地处理这些情况。在生产代码中,它树立了一个坏榜样:一个 unwrap
会吸引另一个,并且代码库会随着你继续沿着这条道路而变得更加脆弱。1
好吧,我已经让你等了很久了。让我们揭开这个错误信息的神秘面纱。
真正的问题
编译器试图告诉我们的是你不能在返回 Result
的函数中传播 optionals。
如果你返回一个 Option
,一切都会正常工作:
fn get_user_name() -> Option<String> {
// 可以运行 :)
let user = get_user()?;
// 对 `user` 做一些处理
// ...
Some(user)
}
因此,如果你可以更改外部函数以返回 Option
,你就不会遇到上面的错误信息。 Rust 文档中有更多信息 这里。
但是,如果你的函数的最终返回类型必须是 Result
,或者你想向调用者传递关于缺失值的更多信息呢?毕竟,传达不同 None
值之间的区别对你的函数的用户来说可能很有用。
那么,如果你 真的 希望你的代码看起来像这样呢?
fn get_user_name() -> Result<String, String> {
// 不起作用,因为我们返回一个 `Result`
let user = get_user()?;
// 更多可能失败的操作,在这里
// ...
// 返回一个 `Result`
Ok(user)
}
好吧,这只是一个类型错误:get_user()
返回一个 Option
,但外部函数期望一个 Result
。
我们的问题陈述变得更容易:在这种情况下,当 Option
为 None
时,如何返回有用的错误消息?
事实证明,有多种解决方案!
解决方案 1:更改返回类型
如果“没有值”确实是你程序逻辑中的一种错误情况,你应该使用 Result
而不是 Option
。当缺少值是一种正常的、预期的可能性,而不是错误状态时,最好使用 Option
。
在我们的例子中,如果你可以更改 get_user()
以返回 Result
而不是 Option
,你可以像你预期的那样使用 ?
操作符:
fn get_user() -> Result<String, String> {
// ...
Ok("Alice".to_string())
}
fn get_user_name() -> Result<String, String> {
let user = get_user()?;
// 对 `user` 做一些处理
// ...
Ok(user)
}
但是,你可能由于各种原因而无法更改 get_user()
以返回 Result
,例如,如果它是库的一部分,或者它在许多地方使用,或者其他调用者不将缺少用户视为错误。
在这种情况下,请继续阅读!
解决方案 2:ok_or
最初的错误消息虽然隐晦,但给了我们一个提示:
use `.ok_or(...)?` to provide an error compatible with `Result<(), Box<dyn std::error::Error>>`.
显然,我们可以使用 ok_or
方法,它将 Option
转换为 Result
:
let user = get_user().ok_or("No user")?;
这很方便!当我们使用迭代器模式时,它也可以很好地链接:
let user = get_user()
.ok_or("No user")?
.do_something()
.ok_or("Something went wrong")?;
如果你可以从 None
中恢复并优雅地处理这种情况,ok_or
也很棒:
let user = get_user().ok_or(get_logged_in_username())?;
这些是 ok_or
的良好用例。
另一方面,对于每个人来说,ok_or
的作用可能不是很明显。我发现 ok_or
这个名字很不直观,需要查找很多次。这是因为 Ok
通常与 Result
类型相关联,而不是 Option
。有 Option::Some
,所以它可以被称为 some_or
,实际上在 2014 年就有人建议过,但是 ok_or
这个名字胜出了,因为 ok_or(MyError)
读起来很顺畅,我能理解为什么。我想我们现在必须忍受这种轻微的不一致性了。
我认为 ok_or
是解决问题的快速方法,但还有一些可能更具可读性的替代方案。
解决方案 3:match
过去,我曾经建议人们不要耍花招,而只是使用 match
语句。
let user = match get_user() {
Some(user) => user,
None => return Err("No user".into()),
};
match
与 Option
结合使用,因为它只是一个枚举,我们可以对它进行模式匹配。只要我们涵盖所有情况,编译器就会很高兴。在这种情况下,我们只有两种情况:Some
和 None
。在 None
情况下,我们提前返回一个错误。在 Some
情况下,我们继续使用该值。(match
是一个表达式,并且块中最后一个表达式的值被返回。在我们的例子中,它是 user
,它被分配给外部范围中的 user
变量。)
这已经更明确,并且对于初学者来说更容易理解。我过去在教授这个时遇到的一个问题是,对于简单的情况,它看起来有点冗长。
解决方案 4:let-else
使用 Rust 1.65,let-else
表达式被稳定了,所以现在你可以这样写:
let Some(user) = get_user() else {
return Err("No user".into());
};
// 对 user 做一些处理
在我看来,这是两全其美:它既紧凑又易于理解。它受到初学者和经验丰富的 Rustacean 的一致喜爱。
一些解释:如果 get_user()
返回 Some
,let
语句将解构 Some
变体并将该值分配给 user
变量。如果 get_user()
返回 None
,则将执行 else
块,并且我们提前返回一个错误。
我最喜欢 let-else
的地方是它清楚地突出了代码的“happy path”。
与需要阅读两个分支才能理解预期流程的 match
语句不同,let-else
可以立即清楚地知道预期的情况是什么,而 else
块处理异常情况。
这是一个明显的赢家。对于初学者来说,它更直观;一旦他们理解了这个模式,他们就会一直使用它!
额外内容:anyhow
我想在这里添加一个荣誉奖。
如果你正在编写一个应用程序(不是库),并且你已经在使用 anyhow
crate 了,你也可以使用他们的 context
方法来处理 None
:
use anyhow::{Context, Result};
fn get_user_name() -> Result<String, anyhow::Error> {
let user = get_user().context("No user")?;
// 对 `user` 做一些处理
Ok(user)
}
它比 let-else
稍微简洁一些,这使得它很有吸引力。只需记住 anyhow
是一个外部依赖项。对于应用程序来说,它可能没问题,但你可能不想在库中使用它,因为你的库的用户将无法再匹配具体的错误变体。
这就是为什么我相信在大多数情况下,let-else
是处理 None
的最佳解决方案。
- 它是标准库的一部分。
- 它对于初学者来说很容易理解。
- 学习它背后的机制在其他地方也很有帮助。
- 它相当紧凑。
- 如果需要,它允许在
else
块中进行更复杂的错误处理逻辑。
结论
在大多数情况下,我更喜欢这种语法:
let Some(value) = some_function() else {
return Err("描述性的错误消息".into());
};
对我来说,let-else
是处理 None
的最佳解决方案,因为:
- 它是标准库的一部分。
- 它适用于库和应用程序。
- 对于初学者来说很容易理解。
- 它相当紧凑。
- 如果需要,它允许在
else
块中进行更复杂的错误处理逻辑。 - 学习它背后的机制在 Rust 中的其他地方也很有帮助。
我希望这有助于更多的人以更健壮的方式处理 Option
。如果它可以帮助一个人避免一个 unwrap
,那就已经值了。
- 有时,
unwrap()
可以通过减少噪音来提高代码的可读性,尤其是在成功的情况极有可能发生时。当你能够证明失败是不可能的,或者当panic
实际上是失败的所需行为时,可以使用unwrap()
。 Andrew Gallant 写了一篇关于这个的文章,他在其中进行了更详细的介绍。↩