之前阐述了enum
的优点,并展示了match
表达式如何迫使程序员考虑所有可能性;本项探讨了您应该避免match
表达式的情况——至少明确地。
还介绍了Rust标准库提供的两个无处不在的enum
:
- Option<T>表示(
T
型)值可能存在也可能不存在 - Result<T, E>,当返回值(类型为
T
)的操作可能无法成功,而可能返回错误(类型为E
)时。
对于这些特定的enum
,显式使用match
通常会导致代码没有它需要的那么紧凑,而且不是惯用的Rust。
match
不必要的第一种情况是,只有值是相关的,并且没有值(以及任何相关错误)可以忽略。
struct S {
field: Option<i32>,
}
let s = S { field: Some(42) };
match &s.field {
Some(i) => println!("field is {}", i),
None => {}
}
在这种情况下,if let表达式短一行,更重要的是,更清晰:
if let Some(i) = &s.field {
println!("field is {}", i);
}
然而,大多数时候,缺少值和相关的错误将是程序员必须处理的事情。设计软件来应对故障路径是困难的,其中大部分是基本的复杂性,任何句法支持都无法帮助——特别是决定如果操作失败会发生什么。
在某些情况下,正确的决定是执行鸵鸟动作,并明确不应对失败。用明确的match
做这件事将是不必要的冗长:
let result = std::fs::File::open("/etc/passwd");
let f = match result {
Ok(f) => f,
Err(_e) => panic!("Failed to open /etc/passwd!"),
};
Option和Result都提供了一对方法来提取它们的内部值和panic!如果它不存在:打开包装并等待。后者允许对失败时的错误消息进行个性化处理,但无论哪种情况,生成的代码都更短、更简单——错误处理委托给.unwrap() 后缀 (但仍然存在)。
比如读取文件:
let f = std::fs::File::open("/etc/passwd").unwrap();
但是要清楚:这些帮助函数仍然会出错!所以选择使用它们和选择恐慌是一样的。
然而,在许多情况下,错误处理的正确决定是将决定推迟给其他人。在编写库时尤其如此,其中代码可用于库作者无法预见的各种不同环境中。为了使其他人的工作更轻松,更喜欢Result
而不是Option
,即使这可能涉及不同错误类型之间的转换。
Result
还有一个[#must_use]属性,可以引导库用户朝着正确的方向前进——如果使用返回Result
的代码忽略它,编译器将生成警告:
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
允许错误传播,但以一些可见的样板:
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
臂和return Err(...)
表达式:
pub fn find_user(username: &str) -> Result<UserId, std::io::Error> {
let f = std::fs::File::open("/etc/passwd")?;
// ...
}
Rust的新手有时会发现这令人不安:乍一看很难发现问号,导致对代码可能如何工作感到不安。然而,即使只有一个字符,类型系统仍然在工作,确保涵盖相关类型中表达的所有可能性——让程序员专注于主线代码路径,而不会分心。
此外,这些明显的方法调用通常没有成本:它们都是标记为#[inline]的通用函数,因此生成的代码通常会编译为与手动版本相同的机器代码。
这两个因素加在一起意味着您应该更喜欢Option
和Result
变换而不是显式match
表达式。
在前面的示例中,错误类型排列:内部和外部方法都表示错误为std::io::Error。情况往往并非如此;一个函数可能会从各种不同的子库中积累错误,每个子库都使用不同的错误类型。
一般错误映射在之后中讨论;目前,请注意手动映射:
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))
}
};
// ...
}
可以用.map_err()转换更简洁、更惯用地表达:
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
标准特征的实现从内部错误类型创建外部错误类型,那么编译器将自动执行转换,而无需调用.map_err()
这类转变更广泛地推广。问号运算符是一个大锤子;使用Option
和Result
类型的转换方法,将它们操纵到可以成为钉子的位置。
标准库提供了各种各样的转换方法来实现这一点,如下图所示。与之后写的文章一致,方法会引起恐慌!用红色突出显示。
图表中没有涵盖的一个常见情况是处理参考资料。例如,考虑一个可选地包含一些数据的结构。
struct InputData {
payload: Option<Vec<u8>>,
}
如果天真地尝试引用,此struct
上试图将有效负载传递给带有签名(&[u8]) -> Vec<u8>
的加密函数的方法会失败:
impl InputData {
pub fn encrypted(&self) -> Vec<u8> {
encrypt(&self.payload.unwrap_or(vec![]))
}
}
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
:
pub fn encrypted(&self) -> Vec<u8> {
encrypt(self.payload.as_ref().unwrap_or(&vec![]))
}
总结一下:
- 习惯
Option
和Result
的转换,更喜欢Result
而不是Option
。- 当转换涉及引用时,根据需要使用
.as_ref()
。
- 当转换涉及引用时,根据需要使用
- 优先使用它们进行显式
match
操作。 - 特别是,使用它们将结果类型转换为形式,其中
?
操作员适用。