欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解Rust是如何處理錯誤的

 更新時間:2022年11月30日 16:53:42   作者:古明地覺  
程序在運行的過程中,總是會不可避免地產(chǎn)生錯誤,而如何優(yōu)雅地解決錯誤,也是語言的設(shè)計哲學(xué)之一。本文就來和大家來了Rust是如何處理錯誤的,感興趣的可以了解一下

異常的演進

程序在運行的過程中,總是會不可避免地產(chǎn)生錯誤,而如何優(yōu)雅地解決錯誤,也是語言的設(shè)計哲學(xué)之一。那么現(xiàn)有的主流語言是怎么處理錯誤的呢?比如調(diào)用一個函數(shù),如果函數(shù)執(zhí)行的時候出錯了,那么該怎么處理呢。

C 語言

C 是一門古老的語言,通常會以指針作為參數(shù),在函數(shù)內(nèi)部進行解引用,修改指針指向的值。然后用 1 和 0 代表返回值,如果返回 1,則表示修改成功;返回 0,表示修改失敗。

但這種做法有一個缺陷,就是修改失敗時,無法將原因記錄下來。

C++ 和 Python

引入了 Exception,通過 try catch 可以將異常捕獲,相比 C 進步了一些。但它的缺陷是我們不知道被調(diào)用方會拋出什么異常。

Java

引入了 checked exception,方法的所有者可以聲明自己會拋出什么異常,然后調(diào)用者對異常進行處理。在 Java 程序啟動時,拋出大量異常都是司空見慣的事情,并在相應(yīng)的調(diào)用堆棧中將信息完整地記錄下來。至此,Java 的異常不再是異常,而是一種很普遍的結(jié)構(gòu),從良性到災(zāi)難性都有所使用,異常的嚴重性由調(diào)用者來決定。

而像 Go、Rust 這樣的新興語言,則采用了與之不同的方式。它們沒有像傳統(tǒng)的高級語言一樣引入 try cache,因為設(shè)計者認為這會把控制流搞得非常亂。在 Go 和 Rust 里面,錯誤是通過返回值體現(xiàn)的。

比如打開一個文件,如果文件不存在,像 Python 程序就會直接報錯。但 Go 不一樣,Go 在打開文件的時候會同時返回一個文件句柄和 error,如果文件成功打開,那么 error 就是空;如果文件打開失敗,那么 error 就是錯誤原因。

所以對于 Go 而言,在可能出錯的時候,程序會同時返回 value 和 error。如果你要使用 value,那么必須先對 error 進行判斷。

錯誤和異常

我們上面提到了錯誤(Error)和異常(Exception),有很多人分不清這兩者的區(qū)別,我們來解釋一下。

在 Python 里面很少會對錯誤和異常進行區(qū)分,甚至將它們視做同一種概念。但在 Go 和 Rust 里面,錯誤和異常是完全不同的,異常要比錯誤嚴重得多。

當(dāng)出現(xiàn)錯誤時,開發(fā)者是有能力解決的,比如文件不存在。這時候程序并不會有異常產(chǎn)生,而是正常執(zhí)行,只是作為返回值的 error 不為空,開發(fā)者要基于 error 進行下一步處理。

但如果出現(xiàn)了異常,那么一定是代碼寫錯了,開發(fā)者無法處理了。比如索引越界,程序會直接 panic 掉,所以在 Rust 里面異常又叫做不可恢復(fù)的錯誤。

不可恢復(fù)的錯誤

如果在 Rust 里面出現(xiàn)了異常,也就是不可恢復(fù)的錯誤,那么就表示開發(fā)者希望程序立刻中止掉,不要再執(zhí)行下去了。

而不可恢復(fù)的錯誤,除了程序在運行過程中因為某些原因自然產(chǎn)生之外,也可以手動引發(fā)。

fn?main()?{
????println!("程序開始執(zhí)行");
????//?在?Go?里面引發(fā)異常通過?panic?函數(shù)
????//?Rust?則是通過?panic!?宏,還是挺相似的
????panic!("發(fā)生了不可恢復(fù)的錯誤");
????println!("程序不會執(zhí)行到這里");
}

注意 panic! 和 println! 的參數(shù)一致的,都支持字符串格式化輸出。下面看一下輸出結(jié)果:

如果將環(huán)境變量 RUST_BACKTRACE 設(shè)置為 1,還可以顯示調(diào)用棧。

然后除了 panic! 之外,assert 系列的宏也可以生成不可恢復(fù)的錯誤。

fn?main()?{
????//?如果?assert!?里面的布爾值為真,無事發(fā)生
????//?如果為假,那么程序會?panic?掉
????assert!(1?==?2);

????//?assert!(1?==?2)?還可以寫成
????assert_eq!(1,?2);

????//?除了?assert_eq!?外,還有?assert_ne!
????assert_ne!(1,?2);

????//?不過最常用的還是?assert!
}

還有一個宏叫 unimplemented!,當(dāng)我們的代碼還沒有開發(fā)完畢時,為了在別人調(diào)用的時候能夠提示調(diào)用者,便可以使用這個宏。

fn?get_data()?{
????unimplemented!("還沒開發(fā)完畢,by?{}",?"古明地覺");
}

fn?main()?{
????get_data()
}

它和 Python 里的 raise NotImplementedError 是比較相似的。

最后在 Rust 里面還有一個常用的宏,用于表示程序不可能執(zhí)行到某個地方。

fn?divide_by_3(n:?u32)?->?u32?{
????//?找到可以滿足?3?*?i?大于?n?的最小整數(shù)?i
????for?i?in?0?..?{
????????if?3?*?i?>?n?{
????????????return?i;
????????}
????}
????//?顯然程序不可能執(zhí)行到這里
????//?因為?for?循環(huán)是無限進行的,最終一定會 return
????//?但?Rust?在編譯時,從語法上是判斷不出來的
????//?它只知道這個函數(shù)目前不完整,因為如果?for?循環(huán)結(jié)束,
????//?那么返回值就不符合?u32?類型了,盡管我們知道?for?循環(huán)不可能結(jié)束

????//?為此我們可以隨便?return?一個?u32,并寫上注釋
????//?"此處是為了保證函數(shù)簽名合法,但程序不會執(zhí)行到這里"
????//?而更專業(yè)的做法是使用一個宏
????unreachable!("程序不可能執(zhí)行到這里");
}

如果程序真的執(zhí)行到了該宏所在的地方,那么同樣會觸發(fā)一個不可恢復(fù)的錯誤。

以上就是 Rust 里面的幾個用于創(chuàng)建不可恢復(fù)的錯誤的幾個宏。

可恢復(fù)的錯誤

說完了不可恢復(fù)的錯誤,再來看看可恢復(fù)的錯誤,一般稱之為錯誤。在 Go 里面錯誤是通過多返回值實現(xiàn)的,如果程序可能出現(xiàn)錯誤,那么會多返回一個 error,然后根據(jù) error 是否為空來判斷究竟有沒有產(chǎn)生錯誤。所以開發(fā)者必須先對 error 進行處理,然后才可以執(zhí)行下一步,不應(yīng)該對 error 進行假設(shè)。

而 Rust 的錯誤機制和 Go 類似,只不過是通過枚舉實現(xiàn)的,該枚舉叫 Result,我們看一下它的定義。

pub?enum?Result<T,?E>?{
????Ok(T),
????Err(E),
}

如果將定義簡化一下,那么就是這個樣子。可以看到它就是一個簡單的枚舉,并且?guī)в袃蓚€泛型。我們之前也介紹過一個枚舉叫 Option,用來處理空值的,內(nèi)部有兩個成員,分別是 Some 和 None。

然后枚舉 Result 和 Option 一樣,它和內(nèi)部的成員都是可以直接拿來用的,我們實際舉個例子演示一下吧。

//?計算兩個?i32?的商
fn?divide(a:?i32,?b:?i32)?->?Result<i32,?&'static?str>?{
????let?ret:?Result<i32,?&'static?str>;
????//?如果?b?!=?0,返回?Ok(a?/?b)
????if?b?!=?0?{
????????ret?=?Ok(a?/?b);
????}?else?{
????????//?否則返回除零錯誤
????????ret?=?Err("ZeroDivisionError:?division?by?zero")
????}
????return?ret;
}

fn?main()?{
??? let?a?=?divide(100,?20);
??? println!("a?=?{:?}",?a);

??? let?b?=?divide(100,?0);
??? println!("b?=?{:?}",?b);
??? /*
????a?=?Ok(5)
????b?=?Err("ZeroDivisionError:?division?by?zero")
????*/
}

打印結(jié)果如我們所料,但 Rust 和 Go 一樣,都要求我們提前對 error 進行處理,并且 Rust 比 Go 更加嚴格。對于 Go 而言,在沒有發(fā)生錯誤的時候,即使我們不對 error 做處理(不推薦),也是沒問題的。而 Rust 不管會不會發(fā)生錯誤,都要求對 error 進行處理。

因為 Rust 返回的是枚舉,比如上面代碼中的 a 是一個 Ok(i32),即便沒有發(fā)生錯誤,這個 a 也不能直接用,必須使用 match 表達式處理一下。

fn?main()?{
????//?將返回值和?5?相加,由于?a?是?Ok(i32)
????//?顯然它不能直接和?i32?相加
????let?a?=?divide(100,?20);
????match?a?{
????????Ok(i)?=>?println!("a?+?5?=?{}",?i?+?5),
????????Err(error)?=>?println!("出錯啦:?{}",?error),
????}

????let?b?=?divide(100,?0);
????match?b?{
????????Ok(i)?=>?println!("b?+?5?=?{}",?i?+?5),
????????Err(error)?=>?println!("出錯啦:?{}",?error),
????}
????/*
????a?+?5?=?10
????出錯啦:?ZeroDivisionError:?division?by?zero
????*/
}

雖然這種編碼方式會讓人感到有點麻煩,但它杜絕了出現(xiàn)運行時錯誤的可能。相比運行時報錯,我們寧可在編譯階段多費些功夫。

自定義錯誤和問號表達式

我們說 Rust 為了避免控制流混亂,并沒有引入 try cache 語句。但 try cache 也有它的好處,就是可以完整地記錄堆棧信息,從錯誤的根因到出錯的地方,都能完整地記錄下來,舉個 Python 的例子:

程序報錯了,根因是調(diào)用了函數(shù) f,而出錯的地方是在第 10 行,我們手動 raise 了一個異常??梢钥吹匠绦?qū)⒄麄€錯誤的鏈路全部記錄下來了,只要從根因開始一層層往下定位,就能找到錯誤原因。

而對于 Go 和 Rust 來說就不方便了,特別是 Go,如果每返回一個 error,就打印一次,那么會將 error 打的亂七八糟的。所以我們更傾向于錯誤能夠在上下文當(dāng)中傳遞,對于 Rust 而言,我們可以通過問號表達式來實現(xiàn)這一點。

fn?external_some_func()?->?Result<u32,?&'static?str>?{
????//?外部的某個函數(shù)
????Ok(666)
}

fn?call1()?->?Result<f64,?&'static?str>?{
????//?我們要調(diào)用?external_some_func
????match?external_some_func()?{
????????//?類型轉(zhuǎn)化在?Rust?里面通過?as?關(guān)鍵字
????????Ok(i)?=>?Ok((i?+?1)?as?f64),
????????Err(error)?=>?Err(error)
????}
}

//?但是上面這種調(diào)用方式有點繁瑣
//?我們還可以使用問號表達式
fn?call2()?->?Result<f64,?&'static?str>?{
????//?注:使用問號表達式有一個前提
????//?調(diào)用方和被調(diào)用方的返回值都要是?Result?枚舉類型
????//?并且它們的錯誤類型要相同,比如這里都是?&'static?str
????let?ret?=?external_some_func()?;
????Ok((ret?+?1)?as?f64)
}

fn?main()?{
????println!("{:?}",?call1());??//?Ok(667.0)
????println!("{:?}",?call2());??//?Ok(667.0)
}

里面的 call1 和 call2 是等價的,如果在 call2 里面函數(shù)調(diào)用出錯了,那么會自動將錯誤返回。并且注意 call2 里面的 ret,它是 u32,不是 Ok(u32)。因為函數(shù)調(diào)用出錯會直接返回,不出錯則會將 Ok 里面的 u32 取出來賦值給 ret。

然后我們說如果 external_some_func 函數(shù)執(zhí)行出錯了,那么 call2 就直接將錯誤返回了,程序不會再往下執(zhí)行。所以這也側(cè)面要求,call2 和 external_some_func 的返回值類型都是 Result,并且里面的錯誤類型也要一樣,否則函數(shù)簽名是不合法的。

fn?external_some_func()?->?Result<u32,?&'static?str>?{
????//?外部的某個函數(shù)
????Err("函數(shù)執(zhí)行出錯")
}

fn?call1()?->?Result<f64,?&'static?str>?{
????match?external_some_func()?{
????????Ok(i)?=>?Ok((i?+?1)?as?f64),
????????Err(error)?=>?Err(error)
????}
}

fn?call2()?->?Result<f64,?&'static?str>?{
????let?ret?=?external_some_func()?;
????Ok((ret?+?1)?as?f64)
}

fn?main()?{
????println!("{:?}",?call1());??//?Err("函數(shù)執(zhí)行出錯")
????println!("{:?}",?call2());??//?Err("函數(shù)執(zhí)行出錯")
}

此時錯誤就自動地在上下文當(dāng)中傳遞了,并且還更簡潔,只需要在函數(shù)調(diào)用后面加一個問號即可。

再來考慮一種更復(fù)雜的情況,我們在調(diào)用函數(shù)的時候可能會調(diào)用多個函數(shù),而這多個函數(shù)的錯誤類型不一樣該怎么辦呢?

struct?FileNotFoundError?{
????err:?String,
????filename:?String,
}

struct?IndexError?{
????err:?&'static?str,
????index:?u32,
}

fn?external_some_func1()?->?Result<u32,?FileNotFoundError>?{
????Err(FileNotFoundError?{
????????err:?String::from("文件不存在"),
????????filename:?String::from("main.py"),
????})
}

fn?external_some_func2()?->?Result<i32,?IndexError>?{
????Err(IndexError?{
????????err:?"索引越界了",
????????index:?9,
????})
}

很多時候,錯誤并不是一個簡單的字符串,因為那樣能攜帶的信息太少?;旧隙际且粋€結(jié)構(gòu)體,文字格式的錯誤信息只是里面的字段之一,而其它字段則負責(zé)描述更加詳細的上下文信息。

我們上面有兩個函數(shù),是一會兒我們要調(diào)用的,但問題是它們返回的錯誤類型不同,也就是 Result<T, E> 里面的 E 不同。而如果是這種情況的話,問號表達式就會失效,那么我們應(yīng)該怎么做呢?

//?其它代碼不變
#[derive(Debug)]
enum?MyError?{
????Error1(FileNotFoundError),
????Error2(IndexError)
}

//?為?MyError?實現(xiàn)?From?trait
//?分別是?From<FileNotFoundError>?和?From<IndexError>
impl?From<FileNotFoundError>?for?MyError?{
????fn?from(error:?FileNotFoundError)?->?MyError?{
????????MyError::Error1(error)
????}
}

impl?From<IndexError>?for?MyError?{
????fn?from(error:?IndexError)?->?MyError?{
????????MyError::Error2(error)
????}
}

fn?call1()?->?Result<i32,?MyError>{
????//?調(diào)用的兩個函數(shù)、和當(dāng)前函數(shù)返回的錯誤類型都不相同
????//?但是當(dāng)前函數(shù)是合法的,因為?MyError?實現(xiàn)了?From?trait
????//?當(dāng)錯誤類型是?FileNotFoundError?或?IndexError?時
????//?它們會調(diào)用?MyError?實現(xiàn)的?from?方法
????//?然后將錯誤統(tǒng)一轉(zhuǎn)換為?MyError?類型
????let?x?=?external_some_func1()?;
????let?y?=?external_some_func2()?;
????Ok(x?as?i32?+?y)
}

fn?call2()?->?Result<i32,?MyError>{
????let?y?=?external_some_func2()?;
????let?x?=?external_some_func1()?;
????Ok(x?as?i32?+?y)
}

fn?main()?{
????println!("{:?}",?call1());
????/*
????Err(Error1(FileNotFoundError?{?err:?"文件不存在",?filename:?"main.py"?}))
????*/
????println!("{:?}",?call2());
????/*
????Err(Error2(IndexError?{?err:?"索引越界了",?index:?9?}))
????*/
}

如果調(diào)用的多個函數(shù)返回的錯誤類型相同,那么只需要保證調(diào)用方也返回相同的錯誤類型,即可使用問號表達式。但如果調(diào)用的多個函數(shù)返回的錯誤類型不同,那么這個時候調(diào)用方就必須使用一個新的錯誤類型,其數(shù)據(jù)結(jié)構(gòu)通常為枚舉。

而枚舉里的成員要包含所有可能發(fā)生的錯誤類型,比如這里的FileNotFoundError和IndexError。然后為枚舉實現(xiàn) From trait,該 trait 帶了一個泛型,并且內(nèi)部定義了一個 from 方法。

我們在實現(xiàn)之后,當(dāng)出現(xiàn) FileNotFoundError 和 IndexError 的時候,就會調(diào)用 from 方法,轉(zhuǎn)成調(diào)用方的 MyError 類型,然后返回。

因此這就是 Rust 處理錯誤的方式,可能有一些難理解,需要私下多琢磨琢磨。最后再補充一點,我們知道 main 函數(shù)應(yīng)該返回一個空元組,但除了空元組之外,它也可以返回一個 Result。

fn?main()?->?Result<(),?MyError>?{
????//?如果?call1()?的后面沒有加問號
????//?那么在調(diào)用沒有出錯的時候,返回的就是?Ok(...)
????//?調(diào)用出錯的時候,返回的就是?Err(...)
????//?但不管哪一種,都是?Result<T,?E>?類型
????println!("{:?}",?call1());

????//?如果加了???那么就不一樣了
????//?在調(diào)用沒出錯的時候,會直接將?Ok(...)?里面的值取出來
????//?調(diào)用出錯的時候,當(dāng)前函數(shù)會中止運行,
????//?并將被調(diào)用方(這里是?call2)的錯誤作為調(diào)用方(這里是?main)的返回值返回
????//?此時通過問號表達式,就實現(xiàn)了錯誤在上下文當(dāng)中傳遞
????//?所以這也要求被調(diào)用方返回的錯誤類型要和調(diào)用方相同
????println!("{:?}",?call2()?);

????//?為了使函數(shù)簽名合法,這里要返回一個值,直接返回?Ok(())?即可
????//?但上面的?call2()??是會報錯的,所以它下面的代碼都不會執(zhí)行
????Ok(())
}

我們執(zhí)行一下看看輸出:

由于 main 函數(shù)已經(jīng)是最頂層的調(diào)用方了,所以出錯的時候,直接將錯誤拋出來了。

小結(jié)

以上就是 Rust 的錯誤處理,相比其它語言來說,確實難理解了一些。另外從該系列的開始到現(xiàn)在,我們介紹的都屬于基礎(chǔ)內(nèi)容,而且有些地方介紹的還不夠詳細,后續(xù)我們會將這些內(nèi)容以更深入的方式做一個補充。

到此這篇關(guān)于一文帶你了解Rust是如何處理錯誤的的文章就介紹到這了,更多相關(guān)Rust處理錯誤內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 淺談Rust中聲明可見性

    淺談Rust中聲明可見性

    在Rust編程語言中,聲明可見性是一個核心概念,本文主要介紹了Rust中聲明可見性,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-05-05
  • Rust?編程語言中的所有權(quán)ownership詳解

    Rust?編程語言中的所有權(quán)ownership詳解

    這篇文章主要介紹了Rust?編程語言中的所有權(quán)ownership詳解的相關(guān)資料,需要的朋友可以參考下
    2023-02-02
  • vscode搭建rust開發(fā)環(huán)境的圖文教程

    vscode搭建rust開發(fā)環(huán)境的圖文教程

    本文主要介紹了vscode搭建rust開發(fā)環(huán)境的圖文教程,文中通過圖文介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2024-08-08
  • Rust語言中的String和HashMap使用示例詳解

    Rust語言中的String和HashMap使用示例詳解

    這篇文章主要介紹了Rust語言中的String和HashMap使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-10-10
  • Rust 入門之函數(shù)和注釋實例詳解

    Rust 入門之函數(shù)和注釋實例詳解

    這篇文章主要為大家介紹了Rust 入門之函數(shù)和注釋實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • Rust 語言的全鏈路追蹤庫 tracing使用方法

    Rust 語言的全鏈路追蹤庫 tracing使用方法

    這篇文章主要介紹了Rust 語言的全鏈路追蹤庫 tracing,接下來就以 tracing 為例,介紹一下trace 的核心概念以及使用方法,需要的朋友可以參考下
    2022-12-12
  • rust中trait的使用方法詳解

    rust中trait的使用方法詳解

    trait用中文來講就是特征,它就是一個標(biāo)記,只不過這個標(biāo)記被用在特定的地方,也就是類型參數(shù)的后面,下面我們就來學(xué)習(xí)一下trait的具體使用方法吧
    2023-12-12
  • Rust處理錯誤的實現(xiàn)方法

    Rust處理錯誤的實現(xiàn)方法

    程序在運行的過程中,總是會不可避免地產(chǎn)生錯誤,而如何優(yōu)雅地解決錯誤,也是語言的設(shè)計哲學(xué)之一。本文就來和大家來了Rust是如何處理錯誤的,感興趣的可以了解一下
    2023-03-03
  • Rust中字符串String集合的具有使用

    Rust中字符串String集合的具有使用

    在Rust中,字符串方法主要位于標(biāo)準(zhǔn)庫的std::string模塊中,這些方法可以幫助我們處理字符串的常見操作,本文主要介紹了Rust中字符串String集合的具有使用,具有一定的參考價值,感興趣的可以了解一下
    2024-04-04
  • Rust 的 into_owned() 方法實例詳解

    Rust 的 into_owned() 方法實例詳解

    into_owned是Rust語言中std::borrow::Cow 枚舉的一個方法,into_owned確保了調(diào)用者獲得數(shù)據(jù)的獨立所有權(quán),無論Cow之前是引用還是已經(jīng)擁有數(shù)據(jù),本文給大家介紹Rust 的 into_owned() 方法,感興趣的的朋友跟隨小編一起看看吧
    2024-03-03

最新評論