原文地址:https://github.com/baoyachi/rust-error-handle
1. 前言
這篇文章寫得比較長(zhǎng)鞍陨,全文讀完大約需要15-20min输钩,如果對(duì)Rust
的錯(cuò)誤處理不清楚或還有些許模糊的同學(xué),請(qǐng)靜下心來(lái)細(xì)細(xì)閱讀大刊。當(dāng)讀完該篇文章后,可以說(shuō)對(duì)Rust
的錯(cuò)誤處理可以做到掌握自如。
筆者花費(fèi)較長(zhǎng)篇幅來(lái)描述錯(cuò)誤處理的來(lái)去扩劝,詳細(xì)介紹其及一步步梳理內(nèi)容,望大家能耐心讀完后對(duì)大家有所幫助职辅。當(dāng)然棒呛,在寫這篇文章之時(shí),也借閱了大量互聯(lián)網(wǎng)資料域携,詳見鏈接見底部參考鏈接
掌握好Rust
的錯(cuò)誤設(shè)計(jì)簇秒,不僅可以提升我們對(duì)錯(cuò)誤處理的認(rèn)識(shí),對(duì)代碼結(jié)構(gòu)秀鞭、層次都有很大的幫助趋观。那廢話不多說(shuō),那我們開啟這段閱讀之旅吧??锋边!
2. 背景
筆者在寫這篇文章時(shí)皱坛,也翻閱一些資料關(guān)于Rust
的錯(cuò)誤處理資料,多數(shù)是對(duì)其一筆帶過(guò)宠默,導(dǎo)致之前接觸過(guò)其他語(yǔ)言的新同學(xué)來(lái)說(shuō)麸恍,上手處理Rust
的錯(cuò)誤會(huì)有當(dāng)頭棒喝的感覺(jué)。找些資料發(fā)現(xiàn)unwrap()也可以解決問(wèn)題搀矫,然后心中暗自竊喜抹沪,程序在運(yùn)行過(guò)程中,因?yàn)楹雎詸z查或程序邏輯判斷瓤球,導(dǎo)致某些情況融欧,程序panic。這可能是我們最不愿看到的現(xiàn)象卦羡,遂又回到起點(diǎn)噪馏,重新去了解Rust
的錯(cuò)誤處理。
這篇文章绿饵,通過(guò)一步步介紹欠肾,讓大家清晰知道Rust
的錯(cuò)誤處理的究竟。介紹在Rust
中的錯(cuò)誤使用及如何處理錯(cuò)誤拟赊,以及在實(shí)際工作中關(guān)于其使用技巧刺桃。
3. unwrap的危害!
下面我們來(lái)看一段代碼,執(zhí)行一下:
fn main() {
let path = "/tmp/dat";
println!("{}", read_file(path));
}
fn read_file(path: &str) -> String {
std::fs::read_to_string(path).unwrap()
}
程序執(zhí)行結(jié)果:
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.
什么,因?yàn)?code>path路徑不對(duì)吸祟,程序竟然崩潰了瑟慈,這個(gè)是我們不能接受的桃移!
unwrap() 這個(gè)操作在rust代碼中,應(yīng)該看過(guò)很多這種代碼葛碧,甚至此時(shí)我們正在使用它借杰。它主要用于Option
或Result
的打開其包裝的結(jié)果。常常我們?cè)诖a中进泼,使用簡(jiǎn)單蔗衡,或快速處理,使用了 unwrap() 的操作缘琅,但是粘都,它是一個(gè)非常危險(xiǎn)的信號(hào)!
可能因?yàn)?strong>沒(méi)有程序檢查或校驗(yàn),潛在的bug可能就出現(xiàn)其中刷袍,使得我們程序往往就panic了。這可能使我們最不愿看到的現(xiàn)象樊展。
在實(shí)際項(xiàng)目開發(fā)中呻纹,程序中可能充斥著大量代碼,我們很難避免unwrap()的出現(xiàn)专缠,為了解決這種問(wèn)題雷酪,我們通過(guò)做code review,或使用腳本工具檢查其降低其出現(xiàn)的可能性。
通常每個(gè)項(xiàng)目都有一些約束涝婉,或許:在大型項(xiàng)目開發(fā)中哥力, 不用unwrap() 方法,使用其他方式處理程序墩弯,unwrap() 的不出現(xiàn)可能會(huì)使得程序的健壯性高出很多吩跋。
這里前提是團(tuán)隊(duì)或大型項(xiàng)目,如果只是寫一個(gè)簡(jiǎn)單例子(demo)就不在本篇文章的討論范疇渔工。因?yàn)橐粋€(gè)Demo的問(wèn)題锌钮,可能只是快速示范或演示,不考慮程序健壯性, unwrap() 的操作可能會(huì)更方便代碼表達(dá)引矩。
可能有人會(huì)問(wèn)梁丘,我們通常跑程序unit test,其中的很多mock數(shù)據(jù)會(huì)有 unwrap() 的操作旺韭,我們只是為了在單元測(cè)試中使得程序簡(jiǎn)單氛谜。這種也能不使用嗎?答案:是的区端,完全可以不使用 unwrap() 也可以做到的值漫。
4. 對(duì)比語(yǔ)言處理錯(cuò)誤
說(shuō)到unwrap(),我們不得不提到rust
的錯(cuò)誤處理珊燎,unwrap() 和Rust
的錯(cuò)誤處理是密不可分的惭嚣。
4.1 golang的錯(cuò)誤處理演示
如果了解golang
的話遵湖,應(yīng)該清楚下面這段代碼的意思:
package main
import (
"io/ioutil"
"log"
)
func main() {
path := "/tmp/dat" //文件路徑
file, err := readFile(path)
if err != nil {
log.Fatal(err) //錯(cuò)誤打印
}
println("%s", file) //打印文件內(nèi)容
}
func readFile(path string) (string, error) {
dat, err := ioutil.ReadFile(path) //讀取文件內(nèi)容
if err != nil { //判斷err是否為nil
return "", err //不為nil,返回err結(jié)果
}
return string(dat), nil //err=nil,返回讀取文件內(nèi)容
}
我們執(zhí)行下程序,打印如下晚吞。執(zhí)行錯(cuò)誤延旧,當(dāng)然,因?yàn)槲覀兘o的文件路徑不存在槽地,程序報(bào)錯(cuò)迁沫。
2020/02/24 01:24:04 open /tmp/dat: no such file or directory
這里,golang
采用多返回值方式捌蚊,程序報(bào)錯(cuò)返回錯(cuò)誤問(wèn)題集畅,通過(guò)判斷 err!=nil 來(lái)決定程序是否繼續(xù)執(zhí)行或終止該邏輯。當(dāng)然缅糟,如果接觸過(guò)golang
項(xiàng)目時(shí)挺智,會(huì)發(fā)現(xiàn)程序中大量充斥著if err!=nil
的代碼,對(duì)此網(wǎng)上有對(duì)if err!=nil
進(jìn)行了很多討論窗宦,因?yàn)檫@個(gè)不在本篇文章的范疇中赦颇,在此不對(duì)其追溯、討論赴涵。
4.2 Rust 錯(cuò)誤處理示例
對(duì)比了golang
代碼媒怯,我們對(duì)照上面的例子,看下在Rust
中如何編寫這段程序髓窜,代碼如下:
fn main() {
let path = "/tmp/dat"; //文件路徑
match read_file(path) { //判斷方法結(jié)果
Ok(file) => { println!("{}", file) } //OK 代表讀取到文件內(nèi)容扇苞,正確打印文件內(nèi)容
Err(e) => { println!("{} {}", path, e) } //Err代表結(jié)果不存在,打印錯(cuò)誤結(jié)果
}
}
fn read_file(path: &str) -> Result<String,std::io::Error> { //Result作為結(jié)果返回值
std::fs::read_to_string(path) //讀取文件內(nèi)容
}
當(dāng)前寄纵,因?yàn)槲覀兘o的文件路徑不存在鳖敷,程序報(bào)錯(cuò),打印內(nèi)容如下:
No such file or directory (os error 2)
在Rust
代表中擂啥,Result
是一個(gè)enum
枚舉對(duì)象,部分源碼如下:
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
的枚舉對(duì)象作為程序的返回值哄陶,通過(guò)Result
來(lái)判斷其結(jié)果,我們使用match
匹配的方式來(lái)獲取Result
的內(nèi)容哺壶,判斷正常(Ok)或錯(cuò)誤(Err)屋吨。
或許,我們大致向上看去山宾,golang
代碼和Rust
代碼沒(méi)有本質(zhì)區(qū)別至扰,都是采用返回值方式,給出程序結(jié)果资锰。下面我們就對(duì)比兩種語(yǔ)言說(shuō)說(shuō)之間區(qū)別:
-
golang
采用多返回值方式敢课,我們?cè)谀玫侥繕?biāo)結(jié)果時(shí)(上面是指文件內(nèi)容file),需要首先對(duì)err
判斷是否為nil
,并且我們?cè)?code>return時(shí),需要給多返回值分別賦值直秆,調(diào)用時(shí)需要對(duì)if err!=nil
做結(jié)果判斷濒募。 -
Rust
中采用Result
的枚舉對(duì)象做結(jié)果返回。枚舉的好處是:多選一圾结。因?yàn)?code>Result的枚舉類型為Ok
和Err
瑰剃,使得我們每次在返回Result
的結(jié)果時(shí),要么是Ok
,要么是Err
筝野。它不需要return
結(jié)果同時(shí)給兩個(gè)值賦值晌姚,這樣的情況只會(huì)存在一種可能性: Ok or Err 。 - golang的函數(shù)調(diào)用需要對(duì)
if err!=nil
做結(jié)果判斷歇竟,因?yàn)檫@段代碼 判斷是手動(dòng)邏輯挥唠,往往我們可能因?yàn)槭韬觯瑢?dǎo)致這段邏輯缺失焕议,缺少校驗(yàn)宝磨。當(dāng)然,我們?cè)诰帉懘a期間可以通過(guò)某些工具lint
掃描出這種潛在bug盅安。 -
Rust
的match
判斷是自動(dòng)打開懊烤,當(dāng)然你也可以選擇忽略其中某一個(gè)枚舉值,我們不在此說(shuō)明。
可能有人發(fā)現(xiàn)宽堆,如果我有多個(gè)函數(shù),需要多個(gè)函數(shù)的執(zhí)行結(jié)果茸习,這樣需要match
代碼多次畜隶,代碼會(huì)不會(huì)是一坨一坨,顯得代碼很臃腫号胚,難看籽慢。是的,這個(gè)問(wèn)題提出的的確是有這種問(wèn)題猫胁,不過(guò)這個(gè)在后面我們講解的時(shí)候箱亿,會(huì)通過(guò)程序語(yǔ)法糖避免多次match
多次結(jié)果的問(wèn)題,不過(guò)我們?cè)诖讼炔粩⒄f(shuō)弃秆,后面將有介紹届惋。
5. Rust中的錯(cuò)誤處理
前面不管是golang
還是Rust
采用return
返回值方式,兩者都是為了解決程序中錯(cuò)誤處理的問(wèn)題菠赚。好了脑豹,前面說(shuō)了這么多,我們還是回歸正題:Rust中是如何對(duì)錯(cuò)誤進(jìn)行處理的衡查?
要想細(xì)致了解Rust
的錯(cuò)誤處理瘩欺,我們需要了解std::error::Error
,該trait的內(nèi)部方法,部分代碼如下:
參考鏈接: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()
在文檔介紹中俱饿,盡管使用它不會(huì)導(dǎo)致編譯警告歌粥,但新代碼應(yīng)該實(shí)現(xiàn)impl Display
,新impl
的可以省略拍埠,不用實(shí)現(xiàn)該方法, 要獲取字符串形式的錯(cuò)誤描述失驶,請(qǐng)使用to_string()
。cause()
在1.33.0被拋棄械拍,取而代之使用source()
方法突勇,新impl
的不用實(shí)現(xiàn)該方法。-
source()
此錯(cuò)誤的低級(jí)源坷虑,如果內(nèi)部有錯(cuò)誤類型Err
返回:Some(e)
,如果沒(méi)有返回:None
甲馋。- 如果當(dāng)前
Error
是低級(jí)別的Error
,并沒(méi)有子Error,需要返回None
。介于其本身默認(rèn)有返回值None
迄损,可以不覆蓋該方法定躏。 - 如果當(dāng)前
Error
包含子Error,需要返回子Error:Some(err)
,需要覆蓋該方法。
- 如果當(dāng)前
type_id()
該方法被隱藏芹敌。backtrace()
返回發(fā)生此錯(cuò)誤的堆棧追溯痊远,因?yàn)闃?biāo)記unstable
,在Rust
的stable
版本不被使用氏捞。自定義的
Error
需要impl std::fmt::Debug的trait,當(dāng)然我們只需要在默認(rèn)對(duì)象上添加注解:#[derive(Debug)]
即可碧聪。
總結(jié)一下,自定義一個(gè)error
需要實(shí)現(xiàn)如下幾步:
- 手動(dòng)實(shí)現(xiàn)impl
std::fmt::Display
的trait,并實(shí)現(xiàn)fmt(...)
方法液茎。 - 手動(dòng)實(shí)現(xiàn)impl
std::fmt::Debug
的trait
逞姿,一般直接添加注解即可:#[derive(Debug)]
- 手動(dòng)實(shí)現(xiàn)impl
std::error::Error
的trait
,并根據(jù)自身error
級(jí)別是否覆蓋std::error::Error
中的source()
方法。
下面我們自己手動(dòng)實(shí)現(xiàn)下Rust
的自定義錯(cuò)誤:CustomError
use std::error::Error;
///自定義類型 Error,實(shí)現(xiàn)std::fmt::Debug的trait
#[derive(Debug)]
struct CustomError {
err: ChildError,
}
///實(shí)現(xiàn)Display的trait捆等,并實(shí)現(xiàn)fmt方法
impl std::fmt::Display for CustomError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CustomError is here!")
}
}
///實(shí)現(xiàn)Error的trait,因?yàn)橛凶覧rror:ChildError,需要覆蓋source()方法,返回Some(err)
impl std::error::Error for CustomError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.err)
}
}
///子類型 Error,實(shí)現(xiàn)std::fmt::Debug的trait
#[derive(Debug)]
struct ChildError;
///實(shí)現(xiàn)Display的trait滞造,并實(shí)現(xiàn)fmt方法
impl std::fmt::Display for ChildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ChildError is here!")
}
}
///實(shí)現(xiàn)Error的trait,因?yàn)闆](méi)有子Error,不需要覆蓋source()方法
impl std::error::Error for ChildError {}
///構(gòu)建一個(gè)Result的結(jié)果,返回自定義的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
,沒(méi)有覆蓋source()
方法栋烤,空實(shí)現(xiàn)了std::error::Error
-
CustomError
有子類型ChildError
,覆蓋了source()
,并返回了子類型Option值:Some(&self.err)
運(yùn)行執(zhí)行結(jié)果谒养,顯示如下:
Error: CustomError is here!
Caused by: ChildError is here!
至此,我們就了解了如何實(shí)現(xiàn)Rust
中自定義Error了明郭。
6. 自定義Error轉(zhuǎn)換:From
上面我們說(shuō)到买窟,函數(shù)返回Result
的結(jié)果時(shí),需要獲取函數(shù)的返回值是成功(Ok)還是失敗(Err)达址,需要使用match
匹配蔑祟,我們看下多函數(shù)之間調(diào)用是如何解決這類問(wèn)題的?假設(shè)我們有個(gè)場(chǎng)景:
- 讀取一文件
- 將文件內(nèi)容轉(zhuǎn)化為
UTF8
格式 - 將轉(zhuǎn)換后格式內(nèi)容轉(zhuǎn)為
u32
的數(shù)字沉唠。
所以我們有了下面三個(gè)函數(shù)(省略部分代碼):
...
///讀取文件內(nèi)容
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉(zhuǎn)換為utf8內(nèi)容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉(zhuǎn)化為u32數(shù)字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
最終疆虚,我們得到u32
的數(shù)字,對(duì)于該場(chǎng)景如何組織我們代碼呢?
-
unwrap()
直接打開三個(gè)方法径簿,取出值罢屈。這種方式太暴力,并且會(huì)有bug
,造成程序panic
,不被采納篇亭。 -
match
匹配缠捌,如何返回OK,繼續(xù)下一步,否則報(bào)錯(cuò)終止邏輯译蒂,那我們?cè)囋嚒?/li>
參考代碼如下:
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)
}
}
}
///讀取文件內(nèi)容
fn read_file(path: &str) -> Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉(zhuǎn)換為utf8內(nèi)容
fn to_utf8(v: &[u8]) -> Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉(zhuǎn)化為u32數(shù)字
fn to_u32(v: &str) -> Result<u32, std::num::ParseIntError> {
v.parse::<u32>()
}
天啊曼月,雖然是實(shí)現(xiàn)了上面場(chǎng)景的需求,但是代碼猶如疊羅漢柔昼,程序結(jié)構(gòu)越來(lái)越深啊哑芹,這個(gè)是我們沒(méi)法接受的!match
匹配導(dǎo)致程序如此不堪一擊捕透。那么有沒(méi)有第三種方法呢聪姿?當(dāng)然是有的:From
轉(zhuǎn)換。
前面我們說(shuō)到如何自定義的Error,如何我們將上面三個(gè)error
收納到我們自定義的Error中乙嘀,將它們?nèi)齻€(gè)Error
變成自定義Error的子Error末购,這樣我們對(duì)外的Result
統(tǒng)一返回自定義的Error。這樣程序應(yīng)該可以改變點(diǎn)什么虎谢,我們來(lái)試試吧盟榴。
#[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
為我們實(shí)現(xiàn)的自定義Error -
CustomError
有三個(gè)子類型Error -
CustomError
分別實(shí)現(xiàn)了三個(gè)子類型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(())
}
///讀取文件內(nèi)容
fn read_file(path: &str) -> std::result::Result<String, std::io::Error> {
std::fs::read_to_string(path)
}
/// 轉(zhuǎn)換為utf8內(nèi)容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, std::str::Utf8Error> {
std::str::from_utf8(v)
}
/// 轉(zhuǎn)化為u32數(shù)字
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)
}
}
其實(shí)我們主要關(guān)心的是這段代碼:
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(())
}
我們使用了?
來(lái)替代原來(lái)的match
匹配的方式曹货。?
使用問(wèn)號(hào)作用在函數(shù)的結(jié)束,意思是:
- 程序接受了一個(gè)
Result<(),CustomError>
自定義的錯(cuò)誤類型讳推。 - 當(dāng)前如果函數(shù)結(jié)果錯(cuò)誤,程序自動(dòng)拋出
Err
自身錯(cuò)誤類型玩般,并包含相關(guān)自己類型錯(cuò)誤信息银觅,因?yàn)槲覀冏隽?code>From轉(zhuǎn)換的操作,該函數(shù)的自身類型錯(cuò)誤會(huì)通過(guò)實(shí)現(xiàn)的From
操作自動(dòng)轉(zhuǎn)化為CustomError
的自定義類型錯(cuò)誤坏为。 - 當(dāng)前如果函數(shù)結(jié)果正確究驴,繼續(xù)之后邏輯,直到程序結(jié)束匀伏。
這樣洒忧,我們通過(guò)From
和?
解決了之前match
匹配代碼層級(jí)深的問(wèn)題,因?yàn)檫@種轉(zhuǎn)換是無(wú)感知的够颠,使得我們?cè)谔幚砗缅e(cuò)誤類型后熙侍,只需要關(guān)心我們的目標(biāo)值即可,這樣不需要顯示對(duì)Err(e)
的數(shù)據(jù)單獨(dú)處理,使得我們?cè)诤瘮?shù)后添加?
后蛉抓,程序一切都是自動(dòng)了庆尘。
還記得我們之前討論在對(duì)比golang
的錯(cuò)誤處理時(shí)的:if err!=nil
的邏輯了嗎,這種因?yàn)橛昧?code>?語(yǔ)法糖使得該段判斷將不再存在巷送。
另外驶忌,我們還注意到,Result
的結(jié)果可以作用在main
函數(shù)上笑跛,
- 是的付魔,
Result
的結(jié)果不僅能作用在main
函數(shù)上 -
Result
還可以作用在單元測(cè)試上,這就是我們文中剛開始提到的:因?yàn)橛辛?code>Result的作用飞蹂,使得我們?cè)诔绦蛑袔缀蹩梢酝耆饤?code>unwrap()的代碼塊几苍,使得程序更輕,大大減少潛在問(wèn)題晤柄,程序組織結(jié)構(gòu)更加清晰擦剑。
下面這是作用在單元測(cè)試上的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
我們?cè)趯?shí)際項(xiàng)目中,會(huì)大量使用如上的Result
結(jié)果芥颈,并且Result
的Err
類型是我們自定義錯(cuò)誤
,導(dǎo)致我們寫程序時(shí)會(huì)顯得非常啰嗦惠勒、冗余
///讀取文件內(nèi)容
fn read_file(path: &str) -> std::result::Result<String, CustomError> {
let val = std::fs::read_to_string(path)?;
Ok(val)
}
/// 轉(zhuǎn)換為utf8內(nèi)容
fn to_utf8(v: &[u8]) -> std::result::Result<&str, CustomError> {
let x = std::str::from_utf8(v)?;
Ok(x)
}
/// 轉(zhuǎn)化為u32數(shù)字
fn to_u32(v: &str) -> std::result::Result<u32, CustomError> {
let i = v.parse::<u32>()?;
Ok(i)
}
我們的程序中,會(huì)大量充斥著這種模板代碼爬坑,Rust
本身支持對(duì)類型自定義纠屋,使得我們只需要重命名Result
即可:
pub type IResult<I> = std::result::Result<I, CustomError>; ///自定義Result類型:IResult
這樣,凡是使用的是自定義類型錯(cuò)誤的Result
都可以使用IResult
來(lái)替換std::result::Result
的類型盾计,使得簡(jiǎn)化程序售担,隱藏Error
類型及細(xì)節(jié),關(guān)注目標(biāo)主體署辉,代碼如下:
///讀取文件內(nèi)容
fn read_file(path: &str) -> IResult<String> {
let val = std::fs::read_to_string(path)?;
Ok(val)
}
/// 轉(zhuǎn)換為utf8內(nèi)容
fn to_utf8(v: &[u8]) -> IResult<&str> {
let x = std::str::from_utf8(v)?;
Ok(x)
}
/// 轉(zhuǎn)化為u32數(shù)字
fn to_u32(v: &str) -> IResult<u32> {
let i = v.parse::<u32>()?;
Ok(i)
}
將std::result::Result<I, CustomError>
替換為:IResult<I>
類型
當(dāng)然族铆,會(huì)有人提問(wèn),如果是多參數(shù)類型怎么處理呢哭尝,同樣哥攘,我們只需將OK
類型變成 tuple (I,O)
類型的多參數(shù)數(shù)據(jù)即可,大概這樣:
pub type IResult<I, O> = std::result::Result<(I, O), CustomError>;
使用也及其簡(jiǎn)單材鹦,只需要返回:I,O的具體類型,舉個(gè)示例:
fn foo() -> IResult<String, u32> {
Ok((String::from("bar"), 32))
}
使用重命名類型的Result
逝淹,使得我們錯(cuò)誤類型統(tǒng)一,方便處理桶唐。在實(shí)際項(xiàng)目中栅葡,可以大量看到這種例子的存在。
8. Option轉(zhuǎn)換
我們知道尤泽,在Rust
中欣簇,需要使用到unwrap()
的方法的對(duì)象有Result
,Option
對(duì)象规脸。我們看下Option
的大致結(jié)構(gòu):
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
本身是一個(gè)enum
對(duì)象,如果該函數(shù)(方法)調(diào)用結(jié)果值沒(méi)有值醉蚁,返回None
,反之有值返回Some(T)
如果我們想獲取Some(T)
中的T
,最直接的方式是:unwrap()
燃辖。我們前面說(shuō)過(guò),使用unwrap()
的方式太過(guò)于暴力网棍,如果出錯(cuò)黔龟,程序直接panic
,這是我們最不愿意看到的結(jié)果滥玷。
Ok,那么我們?cè)囅胂? 利用Option
能使用?
語(yǔ)法糖嗎氏身?如果能用?
轉(zhuǎn)換的話,是不是代碼結(jié)構(gòu)就更簡(jiǎn)單了呢惑畴?我們嘗試下,代碼如下:
#[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
}
執(zhí)行結(jié)果報(bào)錯(cuò):
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`.
提示告訴我們沒(méi)有轉(zhuǎn)換std::convert::From<std::option::NoneError>
蛋欣,但是NoneError
本身是unstable
,這樣我們沒(méi)法通過(guò)From
轉(zhuǎn)換為自定義Error如贷。
本身陷虎,在Rust
的設(shè)計(jì)中,關(guān)于Option
和Result
就是一對(duì)孿生兄弟一樣的存在杠袱,Option
的存在可以忽略異常的細(xì)節(jié)尚猿,直接關(guān)注目標(biāo)主體。當(dāng)然楣富,Option
也可以通過(guò)內(nèi)置的組合器ok_or()
方法將其變成Result
凿掂。我們大致看下實(shí)現(xiàn)細(xì)節(jié):
impl<T> Option<T> {
pub fn ok_or<E>(self, err: E) -> Result<T, E> {
match self {
Some(v) => Ok(v),
None => Err(err),
}
}
}
這里通過(guò)ok_or()
方法通過(guò)接收一個(gè)自定義Error類型,將一個(gè)Option
->Result
纹蝴。好的庄萎,變成Result
的類型,我們就是我們熟悉的領(lǐng)域了塘安,這樣處理起來(lái)就很靈活糠涛。
關(guān)于Option
的其他處理方式,不在此展開解決兼犯,詳細(xì)的可看下面鏈接:
9. 避免unwrap()
有人肯定會(huì)有疑問(wèn)脱羡,如果需要判斷的邏輯,又不用?
這種操作免都,怎么取出Option
或Result
的數(shù)據(jù)呢,當(dāng)然點(diǎn)子總比辦法多帆竹,我們來(lái)看下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)
的方式取出值,當(dāng)前else
的邏輯就可能需要自己處理了栽连。當(dāng)然险领,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)
}
只不過(guò),在處理Result
的判斷時(shí)绢陌,使用的是if let Ok(v)
挨下,這個(gè)和Option
的if let Some(v)
有所不同。
到這里脐湾,unwrap()
的代碼片在項(xiàng)目中應(yīng)該可以規(guī)避了臭笆。補(bǔ)充下,這里強(qiáng)調(diào)了幾次規(guī)避秤掌,就如前所言:團(tuán)隊(duì)風(fēng)格統(tǒng)一愁铺,方便管理代碼,消除潛在危機(jī)闻鉴。
10. 自定義Error同級(jí)轉(zhuǎn)換
我們?cè)陧?xiàng)目中茵乱,一個(gè)函數(shù)(方法)內(nèi)部會(huì)有多次Result
的結(jié)果判斷:?
,假設(shè)我們自定義的全局Error名稱為:GlobalError
。
這時(shí)候孟岛,如果全局有一個(gè)Error
可能就會(huì)出現(xiàn)如下錯(cuò)誤:
std::convert::From<error::GlobalError<A>>` is not implemented for `error::GlobalError<B>
意思是:我們自定義的GlobalError
沒(méi)有通過(guò)From<GlobalError<T>>轉(zhuǎn)換我們自己自定義的GlobalError
瓶竭,那這樣,就等于自己轉(zhuǎn)換自己渠羞。注意:
- 第一:這是我們不期望這樣做的斤贰。
- 第二:遇到這種自己轉(zhuǎn)換自己的
T
類型很多,我們不可能把出現(xiàn)的T
類型通通實(shí)現(xiàn)一遍堵未。
這時(shí)候腋舌,我們考慮自定義另一個(gè)Error了,假設(shè)我們視為:InnnerError
,我們?nèi)值腅rror取名為:GlobalError
渗蟹,我們?cè)谟龅缴厦驽e(cuò)誤時(shí)块饺,返回Result<T,InnerError>
,這樣我們遇到Result<T,GlobalError>
時(shí),只需要通過(guò)From<T>
轉(zhuǎn)換即可雌芽,代碼示例如下:
impl From<InnerError> for GlobalError {
fn from(s: InnerError) -> Self {
Error::new(ErrorKind::InnerError(e))
}
}
上面說(shuō)的這種情況授艰,可能會(huì)在項(xiàng)目中出現(xiàn)多個(gè)自定義Error,出現(xiàn)這種情況時(shí),存在多個(gè)不同Error的std::result::Result<T,Err>
的返回世落。這里的Err
就可以根據(jù)我們業(yè)務(wù)現(xiàn)狀分別反回不同類型了淮腾。最終,只要實(shí)現(xiàn)了From<T>
的trait
可轉(zhuǎn)化為最終期望結(jié)果屉佳。
11. Error常見開源庫(kù)
好了谷朝,介紹到這里,我們應(yīng)該有了非常清晰的認(rèn)知:關(guān)于如何處理Rust
的錯(cuò)誤處理問(wèn)題了武花。但是想想上面的這些邏輯多數(shù)是模板代碼圆凰,我們?cè)趯?shí)際中,大可不必這樣体箕。說(shuō)到這里专钉,開源社區(qū)也有了很多對(duì)錯(cuò)誤處理庫(kù)的支持挑童,下面列舉了一些:
- 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 錯(cuò)誤處理實(shí)戰(zhàn)
這個(gè)例子介紹了如何在https://github.com/Geal/nom
中處理錯(cuò)誤,這里就不展開介紹了跃须,有興趣的可自行閱讀代碼站叼。
詳細(xì)見鏈接:https://github.com/baoyachi/rust-error-handle/blob/master/src/demo_nom_error_handle.rs
14. 總結(jié)
好了,經(jīng)過(guò)上面的長(zhǎng)篇大論菇民,不知道大家是否明白如何自定義處理Error呢了尽楔。大家現(xiàn)在帶著之前的已有的問(wèn)題或困惑,趕緊實(shí)戰(zhàn)下Rust
的錯(cuò)誤處理吧玉雾,大家有疑問(wèn)或者問(wèn)題都可以留言我翔试,希望這篇文章對(duì)你有幫助。
文中代碼詳見:https://github.com/baoyachi/rust-handle-error/tree/master/src