第 3 条:优先选择Option和Result转换,而非显式匹配表达式

第1条 阐述了枚举(enum)的优点,并展示了 match 表达式如何强制程序员考虑所有可能性;这个方法探讨了在某些情况下,你应尽量避免使用 match 表达式 —— 至少是显式地。

第1条 还介绍了 Rust 标准库提供的两个无处不在的枚举:

  • Option<T>,表示一个值(类型为 T)可能存在也可能不存在。
  • Result<T, E>,用于当尝试返回一个值(类型为 T)的操作可能失败,并可能返回一个错误(类型为 E)。

对于这些特定的枚举,显式使用 match 通常会导致代码比实际需要的不够紧凑,而且不符合 Rust 的习惯用法。

第一种不需要使用 match 的情况是,当只关心值本身,而值的缺失(以及任何相关的错误)可以被忽略时。

#![allow(unused)]
fn main() {
struct S {
    field: Option<i32>,
}

let s = S { field: Some(42) };
match &s.field {
    Some(i) => println!("field is {}", i),
    None => {}
}
}

对于这种情况,使用 if let 表达式可以缩短一行代码,而且更重要的是,它的表达更清晰:

#![allow(unused)]
fn main() {
if let Some(i) = &s.field {
    println!("field is {}", i);
}
}

然而,大多数时候,程序员通常的处理方式是提供相应的else分支:缺少值( Option::None ),或者返回一个可能会出现相关错误( Result::Err(e) )。设计能够应对失败路径的软件是困难的,而且其中大部分是本质的复杂性,无论多少语法支持都无法帮助——具体来说,就是决定如果一个操作失败了应该发生什么。 在某些情况下,正确的决定是采取“鸵鸟策略”——把我们的头埋进沙子里,明确地不去处理失败。你不能完全忽略错误分支,因为Rust要求代码必须处理Error枚举的两种变体,但你可以选择将失败视为致命的错误。在失败时执行一个panic!意味着程序会终止,但可以在成功的假设下来编写其余的代码。通过显式match来执行此操作会不必要地冗长:

#![allow(unused)]
fn main() {
let result = std::fs::File::open("/etc/passwd");
let f = match result {
    Ok(f) => f,
    Err(_e) => panic!("Failed to open /etc/passwd!"),
};
}

当值不存在时,OptionResult都提供了一对方法来提取它们的内部值和panic!,它们分别是 unwrapexpect 。后者允许个性化失败时的错误消息,但无论哪种情况,将错误处理被委托给.unwrap()后缀(但仍然存在)生成的代码都更短、更简单:

#![allow(unused)]
fn main() {
let f = std::fs::File::open("/etc/passwd").unwrap();
}

但要明确的是:这些辅助函数仍然会引发 panic!,所以选择使用它们与选择直接 panic!第18条)是一样的。

然而,在许多情况下,正确的错误处理是将决策推迟给其他人。这在编写库时尤其如此,因为库的代码可能会在库作者无法预见的各种不同环境中使用。为了使库更易用,优先使用 Result 而不是 Option来表示错误,即使这可能涉及不同错误类型之间的转换(第4条)。

当然,这提出了一个问题:什么算作错误?在此示例中,无法打开文件肯定是一个错误,并且该错误的详细信息(没有此类文件?权限被拒绝?)可以帮助用户决定下一步要做什么。另一方面,由于切片为空而未能检索切片的first()元素并不是真正的错误,因此它在标准库中表示为Option返回类型。 在两种可能性之间进行选择需要判断,但如果错误可能传达任何有用的信息,则倾向于Result 。

Result 也有一个 [#must_use] 属性,用来引导库用户朝着正确的方向前进 —— 如果使用返回的 Result 的代码忽略了它,编译器将生成一个警告:

#![allow(unused)]
fn main() {
warning: unused `Result` that must be used
  --> transform/src/main.rs:32:5
   |
32 |     f.set_len(0); // Truncate the file
   |     ^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

}

显式使用 match 可以让错误传播,但代价是增加了一些可见的样板代码(让人联想到 Go 语言):

#![allow(unused)]
fn main() {
pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
    let f = match std::fs::File::open("/etc/passwd") {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    // ...
}
}

减少样板代码的关键是 Rust 的问号运算符 ?。这个语法糖可以处理匹配 Err 分支和返回 Err(...) 表达式,只用一个字符就完成了:

#![allow(unused)]
fn main() {
pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
    let f = std::fs::File::open("/etc/passwd")?;
    // ...
}
}

Rust 新手有时会对此感到困惑:问号运算符在一开始很难被注意到,导致人们怀疑这段代码怎么可能正常工作。然而,即使只有一个字符,类型系统仍然在起作用,确保覆盖了相关类型(第1条)表达的所有可能性——让程序员可以专注于主线代码路径,不受干扰。

更重要的是,这些明显的方法调用通常没有额外的成本:它们都是标记为 #[inline] 的泛型函数,所以生成的代码通常会编译成与手动版本相同的机器代码。

这两个因素结合起来意味着你应该优先使用 OptionResult 转换,而不是显式的 match 表达式。

在之前的例子中,错误类型是一致的:内部和外部方法都使用 std::io::Error 表达错误。然而,情况往往并非如此;一个函数可能从各种不同的子库中累积错误,每个子库都使用不同的错误类型。

关于错误映射的讨论一般见第4条;现在,只需知道一个手动映射:

#![allow(unused)]
fn main() {
pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = match std::fs::File::open("/etc/passwd") {
        Ok(f) => f,
        Err(e) => {
            return Err(format!("Failed to open password file: {:?}", e))
        }
    };
    // ...
}
}

可以使用更简洁、更符合 Rust 语法的 .map_err() 转换来表达:

#![allow(unused)]
fn main() {
pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}
}

更好的是,甚至这可能也不必要 —— 如果外部错误类型可以通过实现标准特征 From第5条)从内部错误类型创建,那么编译器将自动执行转换,无需调用 .map_err()

这类转换具有更广泛的通用性。问号运算符是一个强大的工具;使用 OptionResult 类型上的转换方法将它们调整到可以顺利处理的形态。

标准库提供了各种各样的转换方法来实现这一点,如下面的地图所示。根据第18条,可能引发 panic! 的方法用红色突出显示。

转换方法

(此图的在线版本可点击:每个框都会链接到相关文档。)

图中未涵盖的一种常见情况是处理引用。例如,考虑一个可能包含一些数据的结构。

#![allow(unused)]
fn main() {
struct InputData {
    payload: Option<Vec<u8>>,
}
}

这个结构上的一个方法尝试将有效载荷传递给一个加密函数,该函数的签名是 (&[u8]) -> Vec<u8>,如果简单地尝试获取一个引用,则会失败:

#![allow(unused)]
fn main() {
impl InputData {
    pub fn encrypted(&self) -> Vec<u8> {
        encrypt(&self.payload.unwrap_or(vec![]))
    }
}
}
#![allow(unused)]
fn main() {
error[E0507]: cannot move out of `self.payload` which is behind a shared reference
  --> transform/src/main.rs:62:22
   |
62 |             encrypt(&self.payload.unwrap_or(vec![]))
   |                      ^^^^^^^^^^^^ move occurs because `self.payload` has type `Option<Vec<u8>>`, which does not implement the `Copy` trait
   |
help: consider borrowing the `Option`'s content
   |
62 |             encrypt(&self.payload.as_ref().unwrap_or(vec![]))
   |                                  +++++++++
}

错误消息准确地描述了使代码工作所需的内容,即 Option 上的 as_ref() 方法1。这个方法将一个对 Option 的引用转换为对引用的 Option

#![allow(unused)]
fn main() {
pub fn encrypted(&self) -> Vec<u8> {
    encrypt(self.payload.as_ref().unwrap_or(&vec![]))
}
}

总结一下:

  • 习惯使用 OptionResult 的转换,并且优先使用 Result 而不是 Option
  • 在转换涉及引用时,根据需要使用 .as_ref()
  • 在可能的情况下,优先使用它们而不是显式的 match 操作。
  • 特别是,使用它们将结果类型转换成可以使用 ? 运算符的形式。

注释

1

注意,这个方法与 AsRef 特征是分开的,尽管方法名称相同。

原文点这里查看