Rust: 错误处理
错误是软件中不可避免的事实,因此 Rust 具有许多功能来处理出现错误的情况。在许多情况下,Rust 要求您承认错误的可能性并在代码编译之前采取一些措施。此要求可确保您在将代码部署到生产环境之前发现错误并适当处理它们,从而使您的程序更加健壮!
Rust 将错误分为两大类:
- 可恢复错误
- 对于可恢复错误,它的类型为
Result<T, E>
- 对于可恢复错误,它的类型为
- 不可恢复错误
panic!
当程序遇到不可恢复的错误时停止执行的宏。
错误传播:?
0x01. 使用 panic!
处理不可恢复错误
展开堆栈或中止以响应恐慌
默认情况下,当发生恐慌时,程序会开始展开,这意味着 Rust 会回溯堆栈并清理它遇到的每个函数的数据。但是,回溯和清理工作非常繁重。因此,Rust 允许您选择立即中止的替代方案,这将结束程序而不进行清理。
如果您的项目需要使生成的二进制文件尽可能小,则可以通过在Cargo.toml
文件中的相应[profile]
部分中添加panic = 'abort'
,从展开切换到在发生恐慌时中止。例如,如果您想在发布模式下在发生恐慌时中止,请添加以下内容:
1
2 [profile.release]
panic = 'abort'
让我们尝试在一个简单的程序中调用 panic!
:
1 | fn main() { |
运行该程序时,你会看到如下内容:
1 | $ cargo run |
0x02. 使用 Result<T, E>
处理可恢复的错误
大多数错误并不严重到需要程序完全停止。有时,当某个函数失败时,其原因很容易解释和响应。例如,如果您尝试打开一个文件,并且由于该文件不存在而导致该操作失败,您可能希望创建该文件而不是终止该进程。
1 | enum Result<T, E> { |
2.1 错误处理
1 | use std::fs::File; |
2.2 匹配不同的错误
1 | use std::fs::File; |
优化错误处理
1 | use std::fs::File; |
2.3 错误处理的快捷方式:unwrap
和 expect
unwrap
1 | use std::fs::File; |
如果我们在没有 hello.txt
文件的情况下运行此代码,我们将看到来自 unwrap
方法发出的 panic!
调用的错误消息:
1 | thread 'main' panicked at src/main.rs:4:49: |
expect
expect
方法还允许我们选择 panic!
错误消息。使用 expect
而不是 unwrap
并提供良好的错误消息可以传达您的意图,并使追踪 panic
的来源更加容易。expect
的语法如下所示:
1 | use std::fs::File; |
expect
在调用 panic!
时使用的错误消息将是我们传递给 expect
的参数,而不是 unwrap
使用的默认 panic!
消息。它如下所示:
1 | thread 'main' panicked at src/main.rs:5:10: |
2.4 传播错误
当函数的实现调用了可能会失败的某个函数时,您可以将错误返回给调用代码,以便它决定要做什么,而不是在函数本身中处理错误。这称为传播错误,并为调用代码提供更多控制权,其中可能有更多信息或逻辑来指示应如何处理错误,而不是在代码上下文中可用的信息或逻辑。
1 | use std::fs::File; |
2.4.1 传播错误的快捷方式:?
运算符
1 | use std::fs::File; |
2.4.2 在哪里可以使用 ?
运算符
?
运算符只能在返回类型与 ?
所用值兼容的函数中使用。这是因为 ?
运算符被定义为提前从函数中返回一个值。
是提前返回值,不是返回任意值
错误示例
应该返回 ()
,但是 ?
返回了 Result
。
1 | use std::fs::File; |
正确示例
1 | # example 1 |
幸运的是,main
函数也能返回 Result<(), E>
。示例 9-12 的代码来自示例 9-10,但我们将 main 函数的返回类型改为 Result<(), Box<dyn Error>>
,并在末尾添加了返回值 Ok(())
。此代码现在可以通过编译。
1 | # example 2 |
当 main 函数返回 Result<(), E> 时,如果 main 返回 Ok(()),则可执行文件将以 0 值退出;如果 main 返回 Err 值,则可执行文件将以非零值退出。用 C 编写的可执行文件在退出时返回整数:成功退出的程序返回整数 0,出错的程序返回 0 以外的整数。Rust 还会从可执行文件中返回整数以兼容此约定。
0x03. To panic!
or Not to panic!
那么你如何决定何时应该调用 panic!
以及何时应该返回 Result
?当代码崩溃时,没有办法恢复。你可以针对任何错误情况调用 panic!
,无论是否有可能恢复,但你代表调用代码决定这种情况是不可恢复的。当你选择返回 Result
值时,你为调用代码提供了选项。调用代码可以选择尝试以适合其情况的方式进行恢复,或者它可以决定在这种情况下 Err
值是不可恢复的,因此它可以调用 panic!
并将可恢复的错误变成不可恢复的错误。因此,当你定义可能失败的函数时,返回 Result
是一个很好的默认选择。
示例、原型代码和测试
- 当您编写示例来说明某个概念时,同时包含强大的错误处理代码会使示例变得不那么清晰。
- 同样,在您准备好决定如何处理错误之前,unwrap 和 expect 方法在原型设计时非常方便。
- 如果测试中的方法调用失败,您会希望整个测试失败,即使该方法不是被测试的功能。因为 panic! 是将测试标记为失败的方式,所以调用 unwrap 或 expect 正是应该发生的事情。
你比编译器拥有更多信息的情况
如果您有其他逻辑可以确保 Result
具有 Ok
值,但编译器无法理解该逻辑,那么调用 unwrap
或 expect
也是合适的。您仍然需要处理 Result
值:无论您调用什么操作,通常仍有失败的可能性,即使在您的特定情况下逻辑上不可能。如果您可以通过手动检查代码来确保永远不会出现 Err
变体,那么调用 unwrap
是完全可以接受的,甚至最好在 expect
文本中记录您认为永远不会出现 Err 变体的原因。以下是一个例子:
1 | use std::net::IpAddr; |