原文地址:https://github.com/baoyachi/rust-error-handle
1. 前言
這篇文章寫得比較長,全文讀完大約需要15-20min,如果對Rust
的錯誤處理不清楚或還有些許模糊的同學,請靜下心來細細閱讀。當讀完該篇文章后,可以說對Rust
的錯誤處理可以做到掌握自如。
筆者花費較長篇幅來描述錯誤處理的來去,詳細介紹其及一步步梳理內容,望大家能耐心讀完后對大家有所幫助。當然,在寫這篇文章之時,也借閱了大量互聯網資料,詳見鏈接見底部參考鏈接
掌握好Rust
的錯誤設計,不僅可以提升我們對錯誤處理的認識,對代碼結構、層次都有很大的幫助。那廢話不多說,那我們開啟這段閱讀之旅吧??!
2. 背景
筆者在寫這篇文章時,也翻閱一些資料關于Rust
的錯誤處理資料,多數是對其一筆帶過,導致之前接觸過其他語言的新同學來說,上手處理Rust
的錯誤會有當頭棒喝的感覺。找些資料發現unwrap()也可以解決問題,然后心中暗自竊喜,程序在運行過程中,因為忽略檢查或程序邏輯判斷,導致某些情況,程序panic。這可能是我們最不愿看到的現象,遂又回到起點,重新去了解Rust
的錯誤處理。
這篇文章,通過一步步介紹,讓大家清晰知道Rust
的錯誤處理的究竟。介紹在Rust
中的錯誤使用及如何處理錯誤,以及在實際工作中關于其使用技巧。
3. unwrap的危害!
下面我們來看一段代碼,執行一下:
fn main() {
let path = "/tmp/dat";
println!("{}", read_file(path));
}
fn read_file(path: &str) -> String {
std::fs::read_to_string(path).unwrap()
}
程序執行結果:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:1188:5
stack backtrace:
0: backtrace::backtrace::libunwind::trace
at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88
...
15: rust_sugar::read_file
at src/main.rs:7
16: rust_sugar::main
at src/main.rs:3
...
25: rust_sugar::read_file
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
什么,因為path
路徑不對,程序竟然崩潰了,這個是我們不能接受的!
unwrap() 這個操作在rust代碼中,應該看過很多這種代碼,甚至此時我們正在使用它。它主要用于Option
或Result
的打開其包裝的結果。常常我們在代碼中,使用簡單,或快速處理,使用了 unwrap() 的操作,但是,它是一個非常危險的信號!
可能因為沒有程序檢查或校驗,潛在的bug可能就出現其中,使得我們程序往往就panic了。這可能使我們最不愿看到的現象。
在實際項目開發中,程序中可能充斥著大量代碼,我們很難避免unwrap()的出現,為了解決這種問題,我們通過做code review,或使用腳本工具檢查其降低其出現的可能性。
通常每個項目都有一些約束,或許:在大型項目開發中, 不用unwrap() 方法,使用其他方式處理程序,unwrap() 的不出現可能會使得程序的健壯性高出很多。
這里前提是團隊或大型項目,如果只是寫一個簡單例子(demo)就不在本篇文章的討論范疇。因為一個Demo的問題,可能只是快速示范或演示,不考慮程序健壯性, unwrap() 的操作可能會更方便代碼表達。
可能有人會問,我們通常跑程序unit test,其中的很多mock數據會有 unwrap() 的操作,我們只是為了在單元測試中使得程序簡單。這種也能不使用嗎?答案:是的,完全可以不使用 unwrap() 也可以做到的。
4. 對比語言處理錯誤
說到unwrap(),我們不得不提到rust
的錯誤處理,unwrap() 和Rust
的錯誤處理是密不可分的。
4.1 golang的錯誤處理演示
如果了解golang
的話,應該清楚下面這段代碼的意思:
package main
import (
"io/ioutil"
"log"
)
func main() {
path := "/tmp/dat" //文件路徑
file, err := readFile(path)
if err != nil {
log.Fatal(err) //錯誤打印
}
println("%s", file) //打印文件內容
}
func readFile(path string) (string, error) {
dat, err := ioutil.ReadFile(path) //讀取文件內容
if err != nil { //判斷err是否為nil
return "", err //不為nil,返回err結果
}
return string(dat), nil //err=nil,返回讀取文件內容
}
我們執行下程序,打印如下。執行錯誤,當然,因為我們給的文件路徑不存在,程序報錯。
2020/02/24 01:24:04 open /tmp/dat: no such file or directory
這里,golang
采用多返回值方式,程序報錯返回錯誤問題,通過判斷 err!=nil 來決定程序是否繼續執行或終止該邏輯。當然,如果接觸過golang
項目時,會發現程序中大量充斥著if err!=nil
的代碼,對此網上有對if err!=nil
進行了很多討論,因為這個不在本篇文章的范疇中,在此不對其追溯、討論。
4.2 Rust 錯誤處理示例
對比了golang
代碼,我們對照上面的例子,看下在Rust
中如何編寫這段程序,代碼如下:
fn main() {
let path = "/tmp/dat"; //文件路徑
match read_file(path) { //判斷方法結果
Ok(file) => { println!("{}", file) } //OK 代表讀取到文件內容,正確打印文件內容
Err(e) => { println!("{} {}", path, e) } //Err代表結果不存在,打印錯誤結果
}
}
fn read_file(path: &str) -> Result<String,std::io::Error> { //Result作為結果返回值
std::fs::read_to_string(path) //讀取文件內容
}
當前,因為我們給的文件路徑不存在,程序報錯,打印內容如下:
No such file or directory (os error 2)
在Rust
代表中,Result
是一個enum
枚舉對象,部分源碼如下:
pub enum Result<T, E> {
/// Contains the success value
Ok(#[stable(feature = "rust1", since = "1.0.0")] T),
/// Contains the error value
Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}
通常我們使用Result
的枚舉對象作為程序的返回值,通過Result
來判斷其結果,我們使用match
匹配的方式來獲取Result
的內容,判斷正常(Ok)或錯誤(Err)。
或許,我們大致向上看去,golang
代碼和Rust
代碼沒有本質區別,都是采用返回值方式,給出程序結果。下面我們就對比兩種語言說說之間區別:
-
golang
采用多返回值方式,我們在拿到目標結果時(上面是指文件內容file),需要首先對err
判斷是否為nil
,并且我們在return
時,需要給多返回值分別賦值,調用時需要對if err!=nil
做結果判斷。 -
Rust
中采用Result
的枚舉對象做結果返回。枚舉的好處是:多選一。因為Result
的枚舉類型為Ok
和Err
,使得我們每次在返回Result
的結果時,要么是Ok
,要么是Err
。它不需要return
結果同時給兩個值賦值,這樣的情況只會存在一種可能性: Ok or Err 。 - golang的函數調用需要對
if err!=nil
做結果判斷,因為這段代碼 判斷是手動邏輯,往往我們可能因為疏忽,導致這段邏輯缺失,缺少校驗。當然,我們在編寫代碼期間可以通過某些工具lint
掃描出這種潛在bug。 -
Rust
的match
判斷是自動打開,當然你也可以選擇忽略其中某一個枚舉值,我們不在此說明。
可能有人發現,如果我有多個函數,需要多個函數的執行結果,這樣需要match
代碼多次,代碼會不會是一坨一坨,顯得代碼很臃腫,難看。是的,這個問題提出的的確是有這種問題,不過這個在后面我們講解的時候,會通過程序語法糖避免多次match
多次結果的問題,不過我們在此先不敘說,后面將有介紹。
5. Rust中的錯誤處理
前面不管是golang
還是Rust
采用return
返回值方式,兩者都是為了解決程序中錯誤處理的問題。好了,前面說了這么多,我們還是回歸正題:Rust中是如何對錯誤進行處理的?
要想細致了解Rust
的錯誤處理,我們需要了解std::error::Error
,該trait的內部方法,部分代碼如下:
參考鏈接:https://doc.rust-lang.org/std/error/trait.Error.html
pub trait Error: Debug + Display {
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
#[rustc_deprecated(since = "1.33.0", reason = "replaced by Error::source, which can support \
downcasting")]
fn cause(&self) -> Option<&dyn Error> {
self.source()
}
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
#[doc(hidden)]
fn type_id(&self, _: private::Internal) -> TypeId where Self: 'static {
TypeId::of::<Self>()
}
#[unstable(feature = "backtrace", issue = "53487")]
fn backtrace(&self) -> Option<&Backtrace> {
None
}
}
description()
在文檔介紹中,盡管使用它不會導致編譯警告,但新代碼應該實現impl Display
,新impl
的可以省略,不用實現該方法, 要獲取字符串形式的錯誤描述,請使用to_string()
。cause()
在1.33.0被拋棄,取而代之使用source()
方法,新impl
的不用實現該方法。-
source()
此錯誤的低級源,如果內部有錯誤類型Err
返回:Some(e)
,如果沒有返回:None
。- 如果當前
Error
是低級別的Error
,并沒有子Error,需要返回None
。介于其本身默認有返回值None
,可以不覆蓋該方法。 - 如果當前
Error
包含子Error,需要返回子Error:Some(err)
,需要覆蓋該方法。
- 如果當前
type_id()
該方法被隱藏。backtrace()
返回發生此錯誤的堆棧追溯,因為標記unstable
,在Rust
的stable
版本不被使用。自定義的
Error
需要impl std::fmt::Debug的trait,當然我們只需要在默認對象上添加注解:#[derive(Debug)]
即可。
總結一下,自定義一個error
需要實現如下幾步:
- 手動實現impl
std::fmt::Display
的trait,并實現fmt(...)
方法。 - 手動實現impl
std::fmt::Debug
的trait
,一般直接添加注解即可:#[derive(Debug)]
- 手動實現impl
std::error::Error
的trait
,并根據自身error
級別是否覆蓋std::error::Error
中的source()
方法。
下面我們自己手動實現下Rust
的自定義錯誤:CustomError
use std::error::Error;
///自定義類型 Error,實現std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
err: ChildError,
}
///實現Display的trait,并實現fmt方法
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CustomError is here!")
}
}
///實現Error的trait,因為有子Error:ChildError,需要覆蓋source()方法,返回Some(err)
impl std::error::Error for CustomError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.err)
}
}
///子類型 Error,實現std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;
///實現Display的trait,并實現fmt方法
impl std::fmt::Display for ChildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChildError is here!")
}
}
///實現Error的trait,因為沒有子Error,不需要覆蓋source()方法
impl std::error::Error for ChildError {}
///構建一個Result的結果,返回自定義的error:CustomError
fn get_super_error() -> Result<(), CustomError> {
Err(CustomError { err: ChildError })
}
fn main() {
match get_super_error() {
Err(e) => {
println!("Error: {}", e);
println!("Caused by: {}", e.source().unwrap());
}
_ => println!("No error"),
}
}
-
ChildError
為子類型Error
,沒有覆蓋source()
方法,空實現了std::error::Error
-
CustomError
有子類型ChildError
,覆蓋了source()
,并返回了子類型Option值:Some(&self.err)
運行執行結果,顯示如下:
Error: CustomError is here!
Caused by: ChildError is here!
至此,我們就了解了如何實現Rust
中自定義Error了。
6. 自定義Error轉換:From
上面我們說到,函數返回Result
的結果時,需要獲取函數的返回值是成功(Ok)還是失敗(Err),需要使用match
匹配,我們看下多函數之間調用是如何解決這類問題的?假設我們有個場景:
- 讀取一文件
- 將文件內容轉化為
UTF8
格式 - 將轉換后格式內容轉為
u32
的數字。
所以我們有了下面三個函數(省略部分代碼):
...
///讀取文件內容
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉化為u32數字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
最終,我們得到u32
的數字,對于該場景如何組織我們代碼呢?
-
unwrap()
直接打開三個方法,取出值。這種方式太暴力,并且會有bug
,造成程序panic
,不被采納。 -
match
匹配,如何返回OK,繼續下一步,否則報錯終止邏輯,那我們試試。
參考代碼如下:
fn main() {
let path = "./dat";
match read_file(path) {
Ok(v) => {
match to_utf8(v.as_bytes()) {
Ok(u) => {
match to_u32(u) {
Ok(t) => {
println!("num:{:?}", u);
}
Err(e) => {
println!("{} {}", path, e)
}
}
}
Err(e) => {
println!("{} {}", path, e)
}
}
}
Err(e) => {
println!("{} {}", path, e)
}
}
}
///讀取文件內容
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉化為u32數字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
天啊,雖然是實現了上面場景的需求,但是代碼猶如疊羅漢,程序結構越來越深啊,這個是我們沒法接受的!match
匹配導致程序如此不堪一擊。那么有沒有第三種方法呢?當然是有的:From
轉換。
前面我們說到如何自定義的Error,如何我們將上面三個error
收納到我們自定義的Error中,將它們三個Error
變成自定義Error的子Error,這樣我們對外的Result
統一返回自定義的Error。這樣程序應該可以改變點什么,我們來試試吧。
#[derive(Debug)]
enum CustomError {
ParseIntError(std::num::ParseIntError),
Utf8Error(std::str::Utf8Error),
IoError(std::io::Error),
}
impl std::error::Error for CustomError{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
CustomError::IoError(ref e) => Some(e),
CustomError::Utf8Error(ref e) => Some(e),
CustomError::ParseIntError(ref e) => Some(e),
}
}
}
impl Display for CustomError{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
CustomError::IoError(ref e) => e.fmt(f),
CustomError::Utf8Error(ref e) => e.fmt(f),
CustomError::ParseIntError(ref e) => e.fmt(f),
}
}
}
impl From<ParseIntError> for CustomError {
fn from(s: std::num::ParseIntError) -> Self {
CustomError::ParseIntError(s)
}
}
impl From<IoError> for CustomError {
fn from(s: std::io::Error) -> Self {
CustomError::IoError(s)
}
}
impl From<Utf8Error> for CustomError {
fn from(s: std::str::Utf8Error) -> Self {
CustomError::Utf8Error(s)
}
}
-
CustomError
為我們實現的自定義Error -
CustomError
有三個子類型Error -
CustomError
分別實現了三個子類型ErrorFrom
的trait,將其類型包裝為自定義Error的子類型
好了,有了自定義的CustomError
,那怎么使用呢? 我們看代碼:
use std::io::Error as IoError;
use std::str::Utf8Error;
use std::num::ParseIntError;
use std::fmt::{Display, Formatter};
fn main() -> std::result::Result<(),CustomError>{
let path = "./dat";
let v = read_file(path)?;
let x = to_utf8(v.as_bytes())?;
let u = to_u32(x)?;
println!("num:{:?}",u);
Ok(())
}
///讀取文件內容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉化為u32數字
fn to_u32(v: &str) -> std::result::Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
#[derive(Debug)]
enum CustomError {
ParseIntError(std::num::ParseIntError),
Utf8Error(std::str::Utf8Error),
IoError(std::io::Error),
}
impl std::error::Error for CustomError{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
CustomError::IoError(ref e) => Some(e),
CustomError::Utf8Error(ref e) => Some(e),
CustomError::ParseIntError(ref e) => Some(e),
}
}
}
impl Display for CustomError{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
CustomError::IoError(ref e) => e.fmt(f),
CustomError::Utf8Error(ref e) => e.fmt(f),
CustomError::ParseIntError(ref e) => e.fmt(f),
}
}
}
impl From<ParseIntError> for CustomError {
fn from(s: std::num::ParseIntError) -> Self {
CustomError::ParseIntError(s)
}
}
impl From<IoError> for CustomError {
fn from(s: std::io::Error) -> Self {
CustomError::IoError(s)
}
}
impl From<Utf8Error> for CustomError {
fn from(s: std::str::Utf8Error) -> Self {
CustomError::Utf8Error(s)
}
}
其實我們主要關心的是這段代碼:
fn main() -> Result<(),CustomError>{
let path = "./dat";
let v = read_file(path)?;
let x = to_utf8(v.as_bytes())?;
let u = to_u32(x)?;
println!("num:{:?}",u);
Ok(())
}
我們使用了?
來替代原來的match
匹配的方式。?
使用問號作用在函數的結束,意思是:
- 程序接受了一個
Result<(),CustomError>
自定義的錯誤類型。 - 當前如果函數結果錯誤,程序自動拋出
Err
自身錯誤類型,并包含相關自己類型錯誤信息,因為我們做了From
轉換的操作,該函數的自身類型錯誤會通過實現的From
操作自動轉化為CustomError
的自定義類型錯誤。 - 當前如果函數結果正確,繼續之后邏輯,直到程序結束。
這樣,我們通過From
和?
解決了之前match
匹配代碼層級深的問題,因為這種轉換是無感知的,使得我們在處理好錯誤類型后,只需要關心我們的目標值即可,這樣不需要顯示對Err(e)
的數據單獨處理,使得我們在函數后添加?
后,程序一切都是自動了。
還記得我們之前討論在對比golang
的錯誤處理時的:if err!=nil
的邏輯了嗎,這種因為用了?
語法糖使得該段判斷將不再存在。
另外,我們還注意到,Result
的結果可以作用在main
函數上,
- 是的,
Result
的結果不僅能作用在main
函數上 -
Result
還可以作用在單元測試上,這就是我們文中剛開始提到的:因為有了Result
的作用,使得我們在程序中幾乎可以完全摒棄unwrap()
的代碼塊,使得程序更輕,大大減少潛在問題,程序組織結構更加清晰。
下面這是作用在單元測試上的Result
的代碼:
...
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_num() -> std::result::Result<(), CustomError> {
let path = "./dat";
let v = read_file(path)?;
let x = to_utf8(v.as_bytes())?;
let u = to_u32(x)?;
assert_eq!(u, 8);
Ok(())
}
}
7. 重命名Result
我們在實際項目中,會大量使用如上的Result
結果,并且Result
的Err
類型是我們自定義錯誤
,導致我們寫程序時會顯得非常啰嗦、冗余
///讀取文件內容
fn read_file(path: &str) -> std::result::Result<String, CustomError> {
let val = std::fs::read_to_string(path)?;
Ok(val)
}
/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {
let x = std::str::from_utf8(v)?;
Ok(x)
}
/// 轉化為u32數字
fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {
let i = v.parse::<u32>()?;
Ok(i)
}
我們的程序中,會大量充斥著這種模板代碼,Rust
本身支持對類型自定義,使得我們只需要重命名Result
即可:
pub type IResult<I> = std::result::Result<I, CustomError>; ///自定義Result類型:IResult
這樣,凡是使用的是自定義類型錯誤的Result
都可以使用IResult
來替換std::result::Result
的類型,使得簡化程序,隱藏Error
類型及細節,關注目標主體,代碼如下:
///讀取文件內容
fn read_file(path: &str) -> IResult<String> {
let val = std::fs::read_to_string(path)?;
Ok(val)
}
/// 轉換為utf8內容
fn to_utf8(v: &[u8]) -> IResult<&str> {
let x = std::str::from_utf8(v)?;
Ok(x)
}
/// 轉化為u32數字
fn to_u32(v: &str) -> IResult<u32> {
let i = v.parse::<u32>()?;
Ok(i)
}
將std::result::Result<I, CustomError>
替換為:IResult<I>
類型
當然,會有人提問,如果是多參數類型怎么處理呢,同樣,我們只需將OK
類型變成 tuple (I,O)
類型的多參數數據即可,大概這樣:
pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;
使用也及其簡單,只需要返回:I,O的具體類型,舉個示例:
fn foo() -> IResult<String, u32> {
Ok((String::from("bar"), 32))
}
使用重命名類型的Result
,使得我們錯誤類型統一,方便處理。在實際項目中,可以大量看到這種例子的存在。
8. Option轉換
我們知道,在Rust
中,需要使用到unwrap()
的方法的對象有Result
,Option
對象。我們看下Option
的大致結構:
pub enum Option<T> {
/// No value
#[stable(feature = "rust1", since = "1.0.0")]
None,
/// Some value `T`
#[stable(feature = "rust1", since = "1.0.0")]
Some(#[stable(feature = "rust1", since = "1.0.0")] T),
}
Option
本身是一個enum
對象,如果該函數(方法)調用結果值沒有值,返回None
,反之有值返回Some(T)
如果我們想獲取Some(T)
中的T
,最直接的方式是:unwrap()
。我們前面說過,使用unwrap()
的方式太過于暴力,如果出錯,程序直接panic
,這是我們最不愿意看到的結果。
Ok,那么我們試想下, 利用Option
能使用?
語法糖嗎?如果能用?
轉換的話,是不是代碼結構就更簡單了呢?我們嘗試下,代碼如下:
#[derive(Debug)]
enum Error {
OptionError(String),
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Error::OptionError(ref e) => e.fmt(f),
}
}
}
pub type Result<I> = std::result::Result<I, Error>;
fn main() -> Result<()> {
let bar = foo(60)?;
assert_eq!("bar", bar);
Ok(())
}
fn foo(index: i32) -> Option<String> {
if index > 60 {
return Some("bar".to_string());
}
None
}
執行結果報錯:
error[E0277]: `?` couldn't convert the error to `Error`
--> src/main.rs:22:22
|
22 | let bar = foo(60)?;
| ^ the trait `std::convert::From<std::option::NoneError>` is not implemented for `Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required by `std::convert::From::from`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
error: could not compile `hyper-define`.
提示告訴我們沒有轉換std::convert::From<std::option::NoneError>
,但是NoneError
本身是unstable
,這樣我們沒法通過From
轉換為自定義Error。
本身,在Rust
的設計中,關于Option
和Result
就是一對孿生兄弟一樣的存在,Option
的存在可以忽略異常的細節,直接關注目標主體。當然,Option
也可以通過內置的組合器ok_or()
方法將其變成Result
。我們大致看下實現細節:
impl<T> Option<T> {
pub fn ok_or<E>(self, err: E) -> Result<T, E> {
match self {
Some(v) => Ok(v),
None => Err(err),
}
}
}
這里通過ok_or()
方法通過接收一個自定義Error類型,將一個Option
->Result
。好的,變成Result
的類型,我們就是我們熟悉的領域了,這樣處理起來就很靈活。
關于Option
的其他處理方式,不在此展開解決,詳細的可看下面鏈接:
9. 避免unwrap()
有人肯定會有疑問,如果需要判斷的邏輯,又不用?
這種操作,怎么取出Option
或Result
的數據呢,當然點子總比辦法多,我們來看下Option
如何做的:
fn main() {
if let Some(v) = opt_val(60) {
println!("{}", v);
}
}
fn opt_val(num: i32) -> Option<String> {
if num >= 60 {
return Some("foo bar".to_string());
}
None
}
是的,我們使用if let Some(v)
的方式取出值,當前else
的邏輯就可能需要自己處理了。當然,Option
可以這樣做,Result
也一定可以:
fn main() {
if let Ok(v) = read_file("./dat") {
println!("{}", v);
}
}
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
只不過,在處理Result
的判斷時,使用的是if let Ok(v)
,這個和Option
的if let Some(v)
有所不同。
到這里,unwrap()
的代碼片在項目中應該可以規避了。補充下,這里強調了幾次規避,就如前所言:團隊風格統一,方便管理代碼,消除潛在危機。
10. 自定義Error同級轉換
我們在項目中,一個函數(方法)內部會有多次Result
的結果判斷:?
,假設我們自定義的全局Error名稱為:GlobalError
。
這時候,如果全局有一個Error
可能就會出現如下錯誤:
std::convert::From<error::GlobalError<A>>` is not implemented for `error::GlobalError<B>
意思是:我們自定義的GlobalError
沒有通過From<GlobalError<T>>轉換我們自己自定義的GlobalError
,那這樣,就等于自己轉換自己。注意:
- 第一:這是我們不期望這樣做的。
- 第二:遇到這種自己轉換自己的
T
類型很多,我們不可能把出現的T
類型通通實現一遍。
這時候,我們考慮自定義另一個Error了,假設我們視為:InnnerError
,我們全局的Error取名為:GlobalError
,我們在遇到上面錯誤時,返回Result<T,InnerError>
,這樣我們遇到Result<T,GlobalError>
時,只需要通過From<T>
轉換即可,代碼示例如下:
impl From<InnerError> for GlobalError {
fn from(s: InnerError) -> Self {
Error::new(ErrorKind::InnerError(e))
}
}
上面說的這種情況,可能會在項目中出現多個自定義Error,出現這種情況時,存在多個不同Error的std::result::Result<T,Err>
的返回。這里的Err
就可以根據我們業務現狀分別反回不同類型了。最終,只要實現了From<T>
的trait
可轉化為最終期望結果。
11. Error常見開源庫
好了,介紹到這里,我們應該有了非常清晰的認知:關于如何處理Rust
的錯誤處理問題了。但是想想上面的這些邏輯多數是模板代碼,我們在實際中,大可不必這樣。說到這里,開源社區也有了很多對錯誤處理庫的支持,下面列舉了一些:
- https://github.com/rust-lang-nursery/failure
- https://github.com/rust-lang-nursery/error-chain
- https://github.com/dtolnay/anyhow
- https://github.com/dtolnay/thiserror
- https://github.com/tailhook/quick-error
12. 參考鏈接
- https://blog.burntsushi.net/rust-error-handling/
- https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/question-mark-in-main-and-tests.html
- https://doc.rust-lang.org/rust-by-example/error/result.html
- https://doc.rust-lang.org/rust-by-example/error.html
- https://github.com/rust-lang/rust/issues/43301
13 錯誤處理實戰
這個例子介紹了如何在https://github.com/Geal/nom
中處理錯誤,這里就不展開介紹了,有興趣的可自行閱讀代碼。
詳細見鏈接:https://github.com/baoyachi/rust-error-handle/blob/master/src/demo_nom_error_handle.rs
14. 總結
好了,經過上面的長篇大論,不知道大家是否明白如何自定義處理Error呢了。大家現在帶著之前的已有的問題或困惑,趕緊實戰下Rust
的錯誤處理吧,大家有疑問或者問題都可以留言我,希望這篇文章對你有幫助。
文中代碼詳見:https://github.com/baoyachi/rust-handle-error/tree/master/src