Rust的错误处理
概述
Rust 偷师 Haskell,构建了对标Maybe的Option 类型和对标Either 的Result 类型
Option 和 Result
Option 是一个 enum,其定义如下
pub enum Option<T> {None,Some(T)
}
它可以承载有值 / 无值这种最简单的错误类型
Result 是一个更加复杂的 enum, 其定义如下:
#[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 代码中,如果你只想传播错误,不想就地处理,可以用 ?操作符
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 让错误传播的代价和异常处理不相上下,同时又避免了异常处理的诸多问题
? 操作符内部被展开成类似这样的代码
match result {Ok(v) => v,Err(e) => return Err(e.into())
}
所有,我们可以方便写出类似这样的代码,简洁易懂,可读性很强
fut.await?.process()?.next().await?;
整个代码的执行流程如下:
虽然 ? 操作符使用起来非常方便,但要注意在不同的错误类型之间是无法直接使用的,需要实现From trait 在二者之间建立起转换的桥梁,这会带来额外的麻烦
函数式错误处理
Rust 还为Option 和 Result 提供了大量的辅助函数,如 map / map_err / add_then, 你可以很方便地处理数据结构中部分情况
通过这些函数,可以很方便地对错误处理引入 Railroad oriented programming 范式。比如用户注册的流程,你需要校验用户输入,对数据进行处理,转换,然后存入数据中
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 的协议变量
let params : NoiseParams = "Noise_XX_25519_AESGCM_SHA256".parse().unwrap();
如果开发者小小心把协议变量写错了,最佳的方式是立刻panic! 出来,让错误立刻暴露,以便解决这个问题
有些场景下,也希望能够像异常处理那样能够栈回溯,把环境恢复到捕获异常的上下文。Rust 标准库下提供了catch_unwind(), 把调用栈回溯到 catch_unwind 这一刻,作用和其他语言的 try {…} catch {…}
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
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) 来简化错误类型的定义
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 的库定义好你项目中主要的错误类型,并随着项目的深入,不断增加新的错误类型,让系统中所有的潜在错误无所遁形