Rust的错误处理

概述

Rust 偷师 Haskell,构建了对标Maybe的Option 类型和对标Either 的Result 类型

Option 和 Result

Option 是一个 enum,其定义如下

rust 复制代码
pub enum Option<T> {
    None,
    Some(T)
}

它可以承载有值 / 无值这种最简单的错误类型

Result 是一个更加复杂的 enum, 其定义如下:

rust 复制代码
#[must_use = "this `Result` may be an `Err` variant, which should be handle"]
pub enum Result<T, E> {
    Ok(T),
    Err(E)
}

当函数出错时,可以返回Err(E), 否则 Ok(T)

Result 类型声明时还有个must_use 的标注,编译器会对有 must_use 标注的所有类型做特殊处理

  • 如果该类型对应的值没有被显式使用,则会告警。这样,保证错误被妥善处理

?操作符

所以在Rust 代码中,如果你只想传播错误,不想就地处理,可以用 ?操作符

rust 复制代码
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);
}

通过 ?操作符,Rust 让错误传播的代价和异常处理不相上下,同时又避免了异常处理的诸多问题

? 操作符内部被展开成类似这样的代码

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

所有,我们可以方便写出类似这样的代码,简洁易懂,可读性很强

rust 复制代码
fut.await?
    .process()?
    .next()
    .await?;

整个代码的执行流程如下:

虽然 ? 操作符使用起来非常方便,但要注意在不同的错误类型之间是无法直接使用的,需要实现From trait 在二者之间建立起转换的桥梁,这会带来额外的麻烦

函数式错误处理

Rust 还为Option 和 Result 提供了大量的辅助函数,如 map / map_err / add_then, 你可以很方便地处理数据结构中部分情况

通过这些函数,可以很方便地对错误处理引入 Railroad oriented programming 范式。比如用户注册的流程,你需要校验用户输入,对数据进行处理,转换,然后存入数据中

rust 复制代码
Ok(data)
    .add_then(validate)
    .add_then(process)
    .map(transform)
    .and_then(store)
    .map_error(...)

执行流程如下图所示:

此外,Option 和 Result 的互相转换也很方便,这也得益于Rust 构建的强大的函数式编程能力

panic! 和 catch_unwind

使用 Option 和 Result 是 Rust 中处理错误的首选,绝大多数时候我们也应该使用,但Rust 也提供了特殊的异常处理能力

在 Rust 看来,一旦你需要抛出异常,那抛出的一定是严重的错误。所以,Rust 跟 Golang 一样,使用了诸如panic! 这样的字眼警示开发者

  • 想清楚了再使用我

在使用Option 和 Result 类型时,开发者也可以对其unwarp() 或者 expect(), 强制把Option 和 Reulst<T, E> 转换成 T,如果无法完成这种转换,也会panic! 出来

一般而言,panic! 是 不可恢复或者不想恢复错误。希望在此刻,程序终止运行并得到崩溃信息

比如下面的代码,它解析 noise protoco 的协议变量

rust 复制代码
let params : NoiseParams = "Noise_XX_25519_AESGCM_SHA256".parse().unwrap();

如果开发者小小心把协议变量写错了,最佳的方式是立刻panic! 出来,让错误立刻暴露,以便解决这个问题

有些场景下,也希望能够像异常处理那样能够栈回溯,把环境恢复到捕获异常的上下文。Rust 标准库下提供了catch_unwind(), 把调用栈回溯到 catch_unwind 这一刻,作用和其他语言的 try {...} catch {...}

rust 复制代码
use std::painic;

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);
}

当然,和异常处理一样,并不意味你可以溢出这一特性,我想,这也是Rust 把 抛出异常称作 panic!, 而捕获异常称作 catch_unwind 的原因,让初学者望而生畏,不敢轻易使用

catch_unwind 在某些场景下非常有用,比如你在使用 Rust 为 erlang VM 撰写 NIF,你不希望Rust 代码中的任何 panic! 导致 erlang VM 崩溃。

因为崩溃是一个非常不好的体验,它违背了 erlang 的 设计原则: process 可以 let it crash ,但错误代码不该导致 VM 崩溃

你就可以把Rust 代码整个封装在 catch_unwind() 函数所需要传入的闭包中,这样,一旦任何代码中,包括第三方crates 的代码,含有能够导致 panic! 的代码,都会被捕获,并被转换为一个Result

Error trait 错误类型的转换

为了规范这个代表错误的数据类型行为,Rust 定义了 Error trait

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 trait

不过,这样的工作已经有人替人简化了,可以使用 thiserror 和 anyhow 来简化这个步骤。thiserror 提供了一个派生宏(drive macro) 来简化错误类型的定义

rust 复制代码
use thiserror::Error;
#[dervie(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 库,那么thiserror 可以很好地协助你对这个库里所有可能发生的错误进行建模

而anyhow 实现了 anyhow::Error 和 任意符号 Error trait 的错误类型之间的转换,让你可以使用?操作符,不必再手工转换错误类型

anyhow 还可以让你容易抛出一些临时的错误,而不必费力定义错误类型,当然,不提倡滥用这个能力

建议开发前,先用类似 thiserror 的库定义好你项目中主要的错误类型,并随着项目的深入,不断增加新的错误类型,让系统中所有的潜在错误无所遁形

相关推荐
easyboot2 小时前
python的print加入颜色显示
开发语言·python
红烧code2 小时前
【Rust GUI开发入门】编写一个本地音乐播放器(12. 国际化应用-多语言支持)
rust·i18n·gui·slint
alwaysrun2 小时前
Rust与C接口交互
c语言·rust·交互
say_fall3 小时前
精通C语言(1.内存函数)
c语言·开发语言
草莓熊Lotso3 小时前
《吃透 C++ vector:从基础使用到核心接口实战指南》
开发语言·c++·算法
java1234_小锋4 小时前
[免费]基于Python的Flask+Vue进销存仓库管理系统【论文+源码+SQL脚本】
后端·python·flask
-雷阵雨-4 小时前
数据结构——LinkedList和链表
java·开发语言·数据结构·链表·intellij-idea
大飞pkz7 小时前
【设计模式】责任链模式
开发语言·设计模式·c#·责任链模式
gplitems1238 小时前
Gunslinger – Gun Store & Hunting WordPress Theme: A Responsible
开发语言·前端·javascript