每日一R「14」错误处理

395 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

今天我们来学习基础课程中的最后一节,异常处理。在任何一门编程语言当中,异常或错误处理都是非常重要的内容,它用来处理异常流程。处理正常流程的 if / while 等,与异常流程一同组成了程序控制流程。

01-错误处理的主流方法

从程序开发者角度,异常处理主要包括以下三部分内容:

  1. 当错误发生时,使用合适的类型描述或捕获错误;
  2. 错误捕获后,可以选择立即处理错误,也可以选择将错误向调用方传播(propagate);
  3. 最后,将错误以恰当方式展现给用户,并且以合式的方式展示给程序开发者,以便排查错误原因。

接下来,我们一块学习下,其他变成语言都是怎么进行错误处理的?

01.1-通过返回值

C 语言是典型的通过返回值来表征错误的编程语言。例如,fread 接口:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

通过返回值,一个 size_t 类型的值来判断读取了多少数据;如果读取过程中异常了,也是通过返回值来表征。

这种方式有几个典型的缺点:

  • 不直观,从 API 上很难看出会发生什么错误,必须有完备且准确的文档来记录返回值的取值情况;
  • 调用者在得到结果后,必须根据结果选择立即处理错误或者显式地向调用方传播错误;
  • 不太容易得到错误发生时的调用栈,对程序开发者来说比较难定位错误原因;

01.2-通过异常

Java 是典型的使用异常来表征错误的编程语言。Java 中异常又进一步细分为编译时的可检查异常和运行时异常。异常是一种关注点分离的思想,即将错误的产生与错误的处理完全隔离开,调用者不必关心错误,而被调用者也不用要求调用者一定要处理错误。

相比于返回值,异常可以根据调用栈自动向上传播,直到被 try…catch 捕获;或者如果到 main 函数仍未被捕获,则程序异常退出。而且,异常包含了错误发生时的调用栈信息,更容易定位错误发生的深层次原因。

但是,异常也存在不完美的地方:

  • 使用异常表征错误,需要考虑异常安全问题,即异常可能会导致某些资源,例如锁,不能正确得到释放;
  • 程序开发者容易滥用异常系统,大量使用一场来控制程序流程,抛异常与捕获是一个开销相对较高的方式;

01.3-通过类型系统

Haskell 是典型的使用类型系统表征错误的编程语言。这类编程语言会要求通过类型表征错误,使用一个内部包含正常返回类型和错误返回类型的复合类型,通过类型系统来强制错误的处理和传递。典型的类型就是 Haskell 中的 Maybe 与 Either。

Maybe 允许数据包含一个值 Just(a),或者没有值 Nothing。Either 允许数据包含一个左值 Left(a),或者一个右值 Right(b);

通过类型系统的方式,可以解决使用返回值方式中调用者必须立即处理或显式向上传播的缺点,可以借助函数式编程简化错误的处理方式,提高代码的可读性。

02-Rust 的错误处理

Rust 借鉴了许多编程语言中的错误处理方式,特别是 Haskell。Rust 中定义了 Option / Result<T, E> 两个 enum 类型。Option 对标了 Haskell 中的 Maybe,它包含了有值 Some(a) 和无值 None。

pub enum Option<T> {
    None,
    Some(T),
}

Result<T, E> 对标 Haskell 中的 Either,它包含了正确值 Ok(T) 和 错误值 Err(E)。

// 强制使用时必须处理该类型的值,若不处理编译器会提示
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

02.1-错误传播

Rust 为简化开发者工作,提供了 ? 操作符来自动地向上传播错误。? 源自宏定义 try!,使得错误传播的代价和异常处理不相上下。? 操作符在编译时会被展开为类似如下代码:

match result {
  Ok(v) => v,
  Err(e) => return Err(e.into())
}

? 也使得很容易写出函数式编程风格的代码:

fut .await? .process()? .next() .await?;

它的执行流程如图所示:

Untitled.png

02.2-函数式错误处理

Option 和 Result<T, E> 枚举类型还实现了许多辅助函数以提升开发者使用体验,例如:map/map_err/and_then

Untitled 1.png

也正是这种设计,使得 Option 与 Result<T, E> 成为 Rust 中最适合做错误处理的类型,也是 Rust 推荐我们使用的方式。

02.3-panic! 和 catch_unwind

在 Rust 中,出现错误时,会使用 panic! 宏中断程序执行。对 Option 和 Result<T, E> 可以使用 unwarp() 或 expect() 方法来强制将其转换为 T,若失败则会 panic!。

Rust 中的 panic! 表示的是不可恢复的错误。如果想像 try…catch 一样,Rust 提供了 catch_unwind。例如:

use std::panic;

fn main() {
    let result = panic::catch_unwind(|| {
        println!("hello!");
    });
    assert!(result.is_ok());
    let result = panic::catch_unwind(|| {
        panic!("oh no!");
    });
    assert!(result.is_err());
    println!("panic captured: {:#?}", result);
}

02.4-Error trait 和错误类型转换

Result<T, E> 中的 E 是一个错误类型,Rust 中对错误类型的行为进行了规范:

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
    fn backtrace(&self) -> Option<&Backtrace> { ... }
    fn description(&self) -> &str { ... }
    fn cause(&self) -> Option<&dyn Error> { ... }
}

常用的 Error 相关的三方库有:thiserroranyhow

本节课程链接《18|错误处理:为什么Rust的错误处理与众不同?


历史文章推荐