前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust学习笔记Day21 为什么Rust的错误处理与众不同?

Rust学习笔记Day21 为什么Rust的错误处理与众不同?

作者头像
用户1072003
发布2023-02-23 17:01:09
6510
发布2023-02-23 17:01:09
举报
文章被收录于专栏:码上读书

语言优秀的错误处理能力,会大大减少错误对整体流程的破坏,减少我们码农的心智负担。

我们一般处理错误的流程:

  1. 当错误发生时,用合适的错误类型捕获错误。
  2. 捕获到错误后,可以立刻处理,也可以延迟在处理。
  3. 根据不同的错误,返回给用户不同的错误消息。

错误处理的主流方法

主要有三种方法:

一、使用返回值(错误码)

有很多例子 比如:

  • 函数返回值
  • 系统调用 的错误码 ErrorNo
  • 进程退出的错误码 RetVal

在 C 语言中,如果 fopen(filename) 无法打开文件,会返回 NULL,调用者通过判断返回值是否为 NULL,来进行相应的错误处理。

这样有很多局限,返回值本来有自己的语义,非要把错误和返回值混淆在一起,加重了开发者的心智负担。

Golang对返回值做了扩展,可以返回多个参数,可以返回专门的error类型。

代码语言:javascript
复制
func Fread(file *File, b []byte) (n int, err error)

这样就可以把错误和正常的返回区分开来了。 这样一来这个err就会在调用链中显式传播。 所以在Golang的代码中随处可见的

代码语言:javascript
复制
if err != nil {
    // 错误处理……
}
二、使用异常

由于返回值不利于错误的传播,Java等语言使用异常来处理错误。

异常可以看成关注点分离错误的产生和处理是分隔开的,调用者不必关心错误。

程序中任何可能出错的地方,都可以抛出异常;而异常可以通过栈回溯(stack unwind)被一层层自动传递,直到遇到捕获异常的地方,如果回溯到 main 函数还无人捕获,程序就会崩溃。如下图所示:

这样可以简化错误处理流程,解决了返回值传播的问题。

用异常更容易写代码,但当异常安全无法保证时,程序的正确性会受到很大的挑战。 可是保证异常安全的第一个原则就是:避免抛出异常。

异常处理另外一个比较严重的问题是:开发者会滥用异常。异常处理的开销要比处理返回值大得多,滥用会有很多额外的开销。

三、使用类型系统

错误信息既然可以通过已有的类型携带,或者通过多返回值的方式提供,那通过类型来表征错误,用一个内部包含正常返回类型和错误返回类型的复合类型,通过类型系统来强制错误的处理和传递效果更好。(Golang 好像就是这样)

但我们前面提到用返回值返回错误的缺点:错误需要被调用者立即处理,或显式传递。 用类型来处理错误的好处是:可以用函数式编程,简化错误的处理。 如:map、fold等函数,让代码不那么冗余。

Rust错误处理

Rust总结前辈的经验,使用类型系统来构建主要的错误处理流程。 构建了Option类型和Result类型。 代码定义如下:

代码语言:javascript
复制
pub enum Option<T> {
    None,
    Some(T),
}


#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • Option是一个简单的enum, 它可以处理有值/没值 这种最简单的错误类型。
  • Result是一个复杂些的enum。当函数出错时,可以返回Err(E),否则Ok(T)。

可以看到Result类型有must_use, 如果没有使用就会报warning,以保证错误被处理了。

上图中的例子,如果我们不处理read_file的返回值,就开始有提示了。 (那这不是回到了 Golang的 到处都是 if err != nil的情况了吗?)

?操作符

如果执行传播错误,不想当时处理,就用?操作符。这样让错误传播和异常处理不相上下,同时又避免了异常处理带来的问题。

代码语言:javascript
复制
use std::fs::File;
use std::io::Read;

fn read_file(name: &str) -> Result<String, std::io::Error> {
  let mut f = File::open(name)?;
  let mut contents = String::new();
  f.read_to_string(&mut contents)?;
  Ok(contents)
}

?操作符 展开来就类似这样:

代码语言:javascript
复制
match result {
  Ok(v) => v,
  Err(e) => return Err(e.into())
}

我们就能写出这样的函数式编程的代码。

代码语言:javascript
复制
fut
  .await?
  .process()?
  .next()
  .await?;

流程如图:

注意: 在不同错误类型之间是无法直接使用的,需要实现From trait在二者之间建立转换桥梁。

Error trait 和错误类型的转换

Result<T, E> 里 E 是一个代表错误的数据类型。 为了规范这个代表错误的数据类型的行为,Rust 定义了 Error trait:

代码语言:javascript
复制
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 trait。 幸运的是,我们可以用thiserror和anyhow来简化这些步骤。

thiserror 提供了一个派生宏(derive macro)来简化错误类型的定义.

代码语言:javascript
复制
use thiserror::Error;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },
    #[error("unknown data store error")]
    Unknown,
}

小结

错误处理的三种方式:使用返回值、异常处理和类型系统。而 Rust 目前看到的方案:主要用类型系统来处理错误,辅以异常来应对不可恢复的错误。

  1. 相比 C/Golang 直接用返回值的错误处理方式,Rust 在类型上更完备,构建了逻辑更为严谨的 Option 类型和 Result 类型,既避免了错误被不慎忽略,也避免了用啰嗦的表达方式传递错误;
  2. 相对于 C++ / Java 使用异常的方式,Rust 区分了可恢复错误和不可恢复错误,分别使用 Option / Result,以及 panic! / catch_unwind 来应对,更安全高效,避免了异常安全带来的诸多问题;
  3. 对比它的老师 Haskell,Rust 的错误处理更加实用简洁,这得益于它强大的元编程功能,使用 ?操作符来简化错误的传递。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-02-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上读书 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 错误处理的主流方法
  • Rust错误处理
    • ?操作符
      • Error trait 和错误类型的转换
      • 小结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档