您好,登錄后才能下訂單哦!
這篇文章主要介紹“Rust錯誤處理有哪些”,在日常操作中,相信很多人在Rust錯誤處理有哪些問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Rust錯誤處理有哪些”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
錯誤處理是編程語言中很重要的一個方面。目前,錯誤處理的方式分為兩類,第一類是以C語言為首的基于返回值的錯誤處理方案,第二類是以Java語言為首的基于異常的錯誤處理方案。也可以從發生了錯誤是否可恢復來進行分類,例如,C語言中對可恢復的錯誤會使用錯誤碼返回值,對不可恢復的錯誤會直接調用exit
來退出程序;Java的異常體系分為Exception
和Error
,分別對應可恢復錯誤和不可恢復錯誤。在Rust中,錯誤處理的方案和C語言類似,但更加完善好用:對于不可恢復錯誤,使用panic
來處理,使得程序直接退出并可輸出相關信息;對于可恢復錯誤,使用Option
和Result
來對返回值進行封裝,表達能力更強。
對于不可恢復錯誤,Rust提供了panic機制來使得程序迅速崩潰,并報告相應的出錯信息。panic出現的場景一般是:如果繼續執行下去就會有極其嚴重的內存安全問題,這種時候讓程序繼續執行導致的危害比崩潰更嚴重。舉個例子:
fn main() { let v = vec![1, 2, 3]; println!("{:?}", v[6]); }
對于上面的程序,數組v
有三個元素,但索引值是6,所以運行后程序會崩潰并報以下錯誤:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 6', src/main.rs:176:22 stack backtrace: 函數調用棧...
在Rust中,panic的實現機制有兩種方式:
unwind方式:發生panic時,會一層一層地退出函數調用棧,棧內的局部變量還可以正常析構。
abort方式:發生panic時,直接退出整個程序。
默認情況下編譯器使用unwind方式,函數調用棧信息可以幫助我們快速定位發生panic的第一現場;但某些嵌入式系統因資源不足而只能選擇abort方式,可以通過rustc -C panic=abort test.rs
方式指定。
在Rust中,通過unwind方式實現的panic,其內部實現方式基本與C++的異常是一樣的。Rust提供了一些工具函數,可以像try-catch
機制那樣讓用戶在代碼中終止棧展開,例如:
fn main() { std::panic::catch_unwind(|| { let v = vec![1, 2, 3]; println!("{:?}", v[6]); println!("interrupted"); // 沒有輸出 }) .ok(); println!("continue"); // 正常輸出 }
運行程序可以發現,println!("interrupted");
語句沒有執行,因此在上一條語句出發了panic,這個函數調用棧開始銷毀,但std::panic::catch_unwind
阻止了調用棧的繼續展開,因此println!("continue");
得以正常執行。
需要注意的是,不要像try-catch
那樣使用catch_unwind
來進行流程控制,Rust更推薦基于返回值的錯誤處理機制,因為既然發生panic了,就讓程序越早崩潰越好,這有利于調試bug,而使用catch_unwind
會讓錯誤暫時被壓制,從而讓錯誤傳遞到其他位置,導致不容易找到程序崩潰的第一現場。catch_unwind
主要用于以下兩種情況:
在FFI的場景下,若C語言調用了Rust的函數,在Rust內部出現了panic,如果這個panic在Rust內部沒處理好,直接扔到C代碼中去,會導致產生“未定義行為”。
某些高級抽象機制需要阻止棧展開,例如線程池。如果一個線程中出現了panic,我們只希望把這個線程關閉,而不是將整個線程池拖下水。
對于可恢復的錯誤,Rust中提供了基于返回值的方案,主要基于Option<T>
和Result<T, E>
類型。Option<T>
代表返回值要么是空要么是非空,Result<T, E>
代表返回值要么是正常值的要么錯誤值。它們的定義如下:
pub enum Option<T> { /// No value None, /// Some value `T` Some(#[stable(feature = "rust1", since = "1.0.0")] T), } 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<T, E>
的典型用法,FromStr
中的from_str
方法可以通過字符串構造出當前類型的實例,但可能會構造失敗。標準庫中針對bool
類型實現了這個trait,正常情況返回bool
類型的值,異常情況返回ParseBoolError
類型的值:
pub trait FromStr: Sized { /// The associated error which can be returned from parsing. type Err; fn from_str(s: &str) -> Result<Self, Self::Err>; } impl FromStr for bool { type Err = ParseBoolError; fn from_str(s: &str) -> Result<bool, ParseBoolError> { match s { "true" => Ok(true), "false" => Ok(false), _ => Err(ParseBoolError { _priv: () }), } } }
我們再來看一個標準庫中對Option<T>
的典型用法,Iterator
的next
方法要么返回下一個元素,要么無元素可返回,因此使用Option<T>
非常合適。
#[must_use = "iterators are lazy and do nothing unless consumed"] pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; ... }
Option<T>
類型解決了許多編程語言中存在的空指針問題。空指針這個設計在加入編程語言時沒有經過深思熟慮,而只是因為易于實現而已。空指針最大的問題在于,它違背了類型系統的規定。類型規定了數據可能的取值范圍,規定了在這些值上可能的操作,也規定了這些數據代表的含義,還規定了這些數據的存儲方式。但是,一個普通的指針和一個空指針,哪怕它們是同樣的類型,做同樣的操作,所得到的結果是不同的。因此,并不能說空指針和普通指針是同一個類型,空指針在類型系統上打開了一個缺口,引入了一個必須在運行期特殊處理的值,它讓編譯器的類型檢查在此失去了意義。對此,Rust的解決方案是把空指針null從一個值上升為一個類型,用enum類型的Option<T>
的None
來代表空指針,而Rust中的enum要求在使用時必須對enum的每一種可能性都進行處理,因此強迫程序員必須考慮到Option<T>
為None
的情形。C/C++中也增添了類似的設計,但由于前向兼容的問題,無法強制使用,因此其作用也就弱化了很多。
Rust中提供了問號運算符?
語法糖來簡化Result<T, E>
和Option<T>
的使用,問號運算符的意思是,如果結果是Err
,則提前返回,否則繼續執行。?
對應著std::ops::Try
這個trait,編譯器會把expr?
這個表達式自動轉換為以下語義:
match Try::into_result(expr) { Ok(V) => v, Err(e) => return Try::from_error(From::from(e)), }
標準庫中已經為Result<T, E>
和Option<T>
兩個類型實現了Try
:
impl<T> ops::Try for Option<T> { type Ok = T; type Error = NoneError; fn into_result(self) -> Result<T, NoneError> { self.ok_or(NoneError) } fn from_ok(v: T) -> Self { Some(v) } fn from_error(_: NoneError) -> Self { None } } impl<T> ops::Try for Option<T> { type Ok = T; type Error = NoneError; fn into_result(self) -> Result<T, NoneError> { self.ok_or(NoneError) } fn from_ok(v: T) -> Self { Some(v) } fn from_error(_: NoneError) -> Self { None } }
可以看到,對于Result
類型,執行問號運算符時,如果碰到Err
,則調用From
trait做類型轉換,然后中斷當前邏輯提前返回。
需要注意的是,問號運算符的引入給main函數帶來了挑戰,因為問號運算符要求函數返回值是Result
類型,而main函數是fn() -> ()
類型,解決這個問題的辦法就是修改main函數的簽名類型,但這樣又會破壞舊代碼。Rust最終的解決方案是引入了一個trait:
pub trait Termination { /// Is called to get the representation of the value as status code. /// This status code is returned to the operating system. fn report(self) -> i32; } impl Termination for () { #[inline] fn report(self) -> i32 { ExitCode::SUCCESS.report() } } impl<E: fmt::Debug> Termination for Result<(), E> { fn report(self) -> i32 { match self { Ok(()) => ().report(), Err(err) => Err::<!, _>(err).report(), } } } impl Termination for ! { fn report(self) -> i32 { self } } impl<E: fmt::Debug> Termination for Result<!, E> { fn report(self) -> i32 { let Err(err) = self; eprintln!("Error: {:?}", err); ExitCode::FAILURE.report() } } impl Termination for ExitCode { #[inline] fn report(self) -> i32 { self.0.as_i32() } }
main函數的簽名就對應地改成了fn<T: Termination>() -> T
,標準庫為Result類型、()
類型等都實現了這個trait,從而這些類型都可以作為main函數的返回類型了。
到此,關于“Rust錯誤處理有哪些”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。