Rust强制我们承认出错的可能性,并在代码中主动采取行动,让程序更加健壮

Rust将错误分成 可恢复 和 不可恢复,大部分语言不会进行区分,而是统一用异常来处理,Rust没有异常。可恢复对应Result ,不可恢复对应panic!。

我觉得C/C++中对异常处理的支持很有问题,不使用RAII的话,内存管理碰到异常或错误就很容易资源泄漏。有的语言像C++/Java支持用try-catch-finally组合拳,但是这种机制对异常安全性的保证很片面。某些函数利用返回值表示异常,这也可能会导致类型不安全的问题,比如我希望函数返回的int指针不是空,结果返回空指针后我自己也忘了处理,稀里糊涂地做出非法操作。此外对于异常的测试往往是不够的,因为我们很多时候无法穷尽所有异常的可能性来对代码进行检测。类型安全,异常安全很重要。

1、panic! 和不可恢复的错误

出现panic!时,选择程序的动作

默认行为:展开unwinding

  • 回溯栈
  • 清理回溯过程中每一个函数的数据
  • 清理过程有很多工作

选择直接终止abort

  • 不清理数据,直接退出
  • os来清理程序使用的内存
  • 如果希望项目的最终二进制文件越小越好,可以在 Cargo.toml 的 [profile] 部分添加 panic=’abort’

    使用panic!的backtrace

    bt是什么

  • 是一个执行到当前位置时,所有被调用的函数的列表

  • 用来找代码中出问题的地方
  • 使用bt的信息需要debug模式,默认就是debug
  • 开启bt需要指定RUST_BACKTRACE,具体是在命令行输入如下指令

    1. RUST_BACKTRACE=1 cargo run

    bt阅读约定

  • 从头开始读,刚开始是问题的发源地

  • 当前行向上是当前行代码调用的代码;当前行向下是调用当前行代码的代码
  • 中间可能包括Rust核心代码、标准库代码、用到的crate代码等,有时候我们对这些没兴趣

2、Result与可恢复的错误

我们遇到错误时,经常想要走其他步骤,而不是终止整个进程

以打开文件为例

  • File::open 返回 io::Error
    • 其中io::Error有一个kind方法可获取到io::ErrorKind,它是标准库提供的枚举,它的成员对应的io操作可能导致不同的错误类型,我们感兴趣的类型是ErrorKind::NotFound
  • 打开文件f后,需要match打开文件的结果

    • 处理找不到文件的错误时,需要创建文件,并match创建文件可能的问题
    • 最终成功的结果都是返回文件,失败的结果都是panic
      1. use std::fs::File;
      2. use std::io::ErrorKind;
      3. fn main() {
      4. let f = match File::open("hello.txt") {
      5. Ok(file) => file,
      6. Err(error) => match error.kind() {
      7. ErrorKind::NotFound => match File::create("hello.txt"){
      8. Ok(fc) => fc,
      9. Err(e) => panic!("Problem creating the file: {:?}", e),
      10. },
      11. other_error => panic!("Problem opening the file: {:?}", other_error),
      12. }
      13. };
      14. }
  • 我们发现正确时,我们Ok的操作是固定的,都是返回正确操作的结果,而错误处理的部分可以千奇百怪,往往需要各种match

    • unwarp_of_else操作,等价于上份代码
      1. fn main() {
      2. let f = File::open("hello.txt").unwrap_or_else(|error| {
      3. if error.kind() == ErrorKind::NotFound {
      4. File::create("hello.txt").unwrap_or_else(|error| {
      5. panic!("Problem creating the file: {:?}", error);
      6. })
      7. } else {
      8. panic!("Problem opening the file: {:?}", error);
      9. }
      10. });
      11. }

      失败时panic的简写

      match太冗长了,Result定义了很多辅助方法来处理各种情况,比如unwrap

      unwrap

  • 如果Result值是成员Ok,则unwrap返回Ok的值

  • 如果Result值是成员Err,unwrap会帮我们调用panic
  • (如果一个函数里好多个unwrap,那就不方便知道具体是哪个unwrap报错

    expect

  • 类似于unwrap,但expect还额外支持自定义错误信息,方便排错 ```rust use std::fs::File;

fn main() { //thread ‘main’ panicked at ‘called Result::unwrap() on an Err value: Error… let f = File::open(“hello.txt”).unwrap(); //thread ‘main’ panicked at ‘Failed to open hello.txt: Error … let f = File::open(“hello.txt”).expect(“Failed to open hello.txt”); }

  1. <a name="H3d4k"></a>
  2. ## 错误传播的简写
  3. 感觉有点类似于java的throw,当前函数不处理错误,传播到调用链上游去处理
  4. <a name="tKkA3"></a>
  5. #### 在返回Result的调用之后加一个 '?'
  6. - 比如第二行,原本是返回Result类型,加了问号后就成了File类型
  7. - 如果错误,则Err的值将作为整个函数的返回值直接返回,错误传播给了调用者
  8. - 如果Ok,程序则继续执行
  9. - 消除大量样板代码,也便于支持链式调用
  10. ```rust
  11. fn read_username_from_file() -> Result<String, io::Error> {
  12. let mut f = File::open("hello.txt")?;
  13. let mut s = String::new();
  14. f.read_to_string(&mut s)?;
  15. Ok(s)
  16. }
  17. fn read_username_from_file() -> Result<String, io::Error> {
  18. let mut s = String::new();
  19. File::open("hello.txt")?.read_to_string(&mut s)?;
  20. Ok(s)
  21. }

From trait

  • 用来将错误从一种类型转换为另一种类型
  • ? 运算符调用from函数时,运算符收到的错误类型,被转换为由当前函数返回类型所定义的错误类型
  • 只要每个错误类型都实现了from函数,定义如何将自身转成返回的错误类型,就可以用 ?运算符 来自动处理

    其他

  • match的return Err(e)要求返回值类型是Result,所以函数的返回值必须是Result才能和这个return兼容

  • main函数中使用 ? 运算符是会报错的
    • main返回值类型是 ()
    • 只能在返回Result或者其他实现了std::ops::Try类型的函数中使用 ?运算符
  • 如果一个函数不返回Result,并且我们在该函数的实现中调用返回Result的函数时还想使用?运算符,该怎么做呢?
    • 将函数返回值改成Result
    • 处理Result,使用match或者Result提供的其他方法,比如unwrap

3、panic! or not

示例、代码原型和测试,适合panic

  • 示例一些概念时,错误处理太多可能就忽视了概念本身,可以unwrap、expect或者panic
  • 如果测试失败了,我们可能希望相关测试都失败

    当我们很自信对于具体的问题非常清楚了,编译器小题大做了

    我们知道Result一定会是Ok,于是unwrap就挺何时
    比如我们像解析ip地址,而这个地址不是用户输入的,而是我们硬编码的正确地址,那么parse()的时候没必要认认真真地进行异常处理了

    错误处理指导原则

    当可能导致有害状态时使用panic!

  • 有害状态指某种保证、假设、协议等不可变因素被打破的状态,如无效值等

  • 有害状态不包含预期会偶尔发生的错误
  • 在此之后代码的运行不依赖于有害状态

当错误可以预期时Result更合适

  • 比如HTTP请求返回的状态,是出发了限流,而不是其他严重问题的状态

可以创建自定义类型,并将错误处理的流程放到相关接口里,方便使用