Rust 有一套獨(dú)特的處理異常情況的機(jī)制,它并不像其它語(yǔ)言中的 try 機(jī)制那樣簡(jiǎn)單。
首先,程序中一般會(huì)出現(xiàn)兩種錯(cuò)誤:可恢復(fù)錯(cuò)誤和不可恢復(fù)錯(cuò)誤。
可恢復(fù)錯(cuò)誤的典型案例是文件訪問(wèn)錯(cuò)誤,如果訪問(wèn)一個(gè)文件失敗,有可能是因?yàn)樗诒徽加?,是正常的,我們可以通過(guò)等待來(lái)解決。
但還有一種錯(cuò)誤是由編程中無(wú)法解決的邏輯錯(cuò)誤導(dǎo)致的,例如訪問(wèn)數(shù)組末尾以外的位置。
大多數(shù)編程語(yǔ)言不區(qū)分這兩種錯(cuò)誤,并用 Exception (異常)類來(lái)表示錯(cuò)誤。在 Rust 中沒(méi)有 Exception。
對(duì)于可恢復(fù)錯(cuò)誤用 Result<T, E> 類來(lái)處理,對(duì)于不可恢復(fù)錯(cuò)誤使用 panic! 宏來(lái)處理。
本章以前沒(méi)有專門介紹 Rust 宏的語(yǔ)法,但已經(jīng)使用過(guò)了 println! 宏,因?yàn)檫@些宏的使用較為簡(jiǎn)單,所以暫時(shí)不需要徹底掌握它,我們可以用同樣的方法先學(xué)會(huì)使用 panic! 宏的使用方法。
fn main() { panic!("error occured"); println!("Hello, Rust"); }
運(yùn)行結(jié)果:
thread 'main' panicked at 'error occured', src\main.rs:3:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
很顯然,程序并不能如約運(yùn)行到 println!("Hello, Rust") ,而是在 panic! 宏被調(diào)用時(shí)停止了運(yùn)行。
不可恢復(fù)的錯(cuò)誤一定會(huì)導(dǎo)致程序受到致命的打擊而終止運(yùn)行。
讓我們注視錯(cuò)誤輸出的兩行:
第一行輸出了 panic! 宏調(diào)用的位置以及其輸出的錯(cuò)誤信息。
第二行是一句提示,翻譯成中文就是"通過(guò) `RUST_BACKTRACE=1` 環(huán)境變量運(yùn)行以顯示回溯"。接下來(lái)我們將介紹回溯(backtrace)。
緊接著剛才的實(shí)例,我們?cè)?VSCode 中新建一個(gè)終端:
在新建的終端里設(shè)置環(huán)境變量(不同的終端方法不同,這里介紹兩種主要的方法):
如果在 Windows 7 及以上的 Windows 系統(tǒng)版本中,默認(rèn)使用的終端命令行是 Powershell,請(qǐng)使用以下命令:
$env:RUST_BACKTRACE=1 ; cargo run
如果你使用的是 Linux 或 macOS 等 UNIX 系統(tǒng),一般情況下默認(rèn)使用的是 bash 命令行,請(qǐng)使用以下命令:
RUST_BACKTRACE=1 cargo run
然后,你會(huì)看到以下文字:
thread 'main' panicked at 'error occured', src\main.rs:3:5 stack backtrace: ... 11: greeting::main at .\src\main.rs:3 ...
回溯是不可恢復(fù)錯(cuò)誤的另一種處理方式,它會(huì)展開(kāi)運(yùn)行的棧并輸出所有的信息,然后程序依然會(huì)退出。上面的省略號(hào)省略了大量的輸出信息,我們可以找到我們編寫(xiě)的 panic! 宏觸發(fā)的錯(cuò)誤。
此概念十分類似于 Java 編程語(yǔ)言中的異常。實(shí)際上在 C 語(yǔ)言中我們就常常將函數(shù)返回值設(shè)置成整數(shù)來(lái)表達(dá)函數(shù)遇到的錯(cuò)誤,在 Rust 中通過(guò) Result<T, E> 枚舉類作返回值來(lái)進(jìn)行異常表達(dá):
enum Result<T, E> { Ok(T), Err(E), }
在 Rust 標(biāo)準(zhǔn)庫(kù)中可能產(chǎn)生異常的函數(shù)的返回值都是 Result 類型的。例如:當(dāng)我們嘗試打開(kāi)一個(gè)文件時(shí):
use std::fs::File; fn main() { let f = File::open("hello.txt"); match f { Ok(file) => { println!("File opened successfully."); }, Err(err) => { println!("Failed to open the file."); } } }
如果 hello.txt 文件不存在,會(huì)打印 "Failed to open the file."。
當(dāng)然,我們?cè)诿杜e類章節(jié)講到的 if let 語(yǔ)法可以簡(jiǎn)化 match 語(yǔ)法塊:
use std::fs::File; fn main() { let f = File::open("hello.txt"); if let Ok(file) = f { println!("File opened successfully."); } else { println!("Failed to open the file."); } }
如果想使一個(gè)可恢復(fù)錯(cuò)誤按不可恢復(fù)錯(cuò)誤處理,Result 類提供了兩個(gè)辦法:unwrap() 和 expect(message: &str) :
use std::fs::File; fn main() { let f1 = File::open("hello.txt").unwrap(); let f2 = File::open("hello.txt").expect("Failed to open."); }
這段程序相當(dāng)于在 Result 為 Err 時(shí)調(diào)用 panic! 宏。兩者的區(qū)別在于 expect 能夠向 panic! 宏發(fā)送一段指定的錯(cuò)誤信息。
之前所講的是接收到錯(cuò)誤的處理方式,但是如果我們自己編寫(xiě)一個(gè)函數(shù)在遇到錯(cuò)誤時(shí)想傳遞出去怎么辦呢?
fn f(i: i32) -> Result<i32, bool> { if i >= 0 { Ok(i) } else { Err(false) } } fn main() { let r = f(10000); if let Ok(v) = r { println!("Ok: f(-1) = {}", v); } else { println!("Err"); } }
運(yùn)行結(jié)果:
Ok: f(-1) = 10000
這段程序中函數(shù) f 是錯(cuò)誤的根源,現(xiàn)在我們?cè)賹?xiě)一個(gè)傳遞錯(cuò)誤的函數(shù) g :
fn g(i: i32) -> Result<i32, bool> { let t = f(i); return match t { Ok(i) => Ok(i), Err(b) => Err(b) }; }
函數(shù) g 傳遞了函數(shù) f 可能出現(xiàn)的錯(cuò)誤(這里的 g 只是一個(gè)簡(jiǎn)單的實(shí)例,實(shí)際上傳遞錯(cuò)誤的函數(shù)一般還包含很多其它操作)。
這樣寫(xiě)有些冗長(zhǎng),Rust 中可以在 Result 對(duì)象后添加 ? 操作符將同類的 Err 直接傳遞出去:
fn f(i: i32) -> Result<i32, bool> { if i >= 0 { Ok(i) } else { Err(false) } } fn g(i: i32) -> Result<i32, bool> { let t = f(i)?; Ok(t) // 因?yàn)榇_定 t 不是 Err, t 在這里已經(jīng)是 i32 類型 } fn main() { let r = g(10000); if let Ok(v) = r { println!("Ok: g(10000) = {}", v); } else { println!("Err"); } }
運(yùn)行結(jié)果:
Ok: g(10000) = 10000
? 符的實(shí)際作用是將 Result 類非異常的值直接取出,如果有異常就將異常 Result 返回出去。所以,? 符僅用于返回值類型為 Result<T, E> 的函數(shù),其中 E 類型必須和 ? 所處理的 Result 的 E 類型一致。
到此為止,Rust 似乎沒(méi)有像 try 塊一樣可以令任何位置發(fā)生的同類異常都直接得到相同的解決的語(yǔ)法,但這樣并不意味著 Rust 實(shí)現(xiàn)不了:我們完全可以把 try 塊在獨(dú)立的函數(shù)中實(shí)現(xiàn),將所有的異常都傳遞出去解決。實(shí)際上這才是一個(gè)分化良好的程序應(yīng)當(dāng)遵循的編程方法:應(yīng)該注重獨(dú)立功能的完整性。
但是這樣需要判斷 Result 的 Err 類型,獲取 Err 類型的函數(shù)是 kind()。
use std::io; use std::io::Read; use std::fs::File; fn read_text_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path)?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } fn main() { let str_file = read_text_from_file("hello.txt"); match str_file { Ok(s) => println!("{}", s), Err(e) => { match e.kind() { io::ErrorKind::NotFound => { println!("No such file"); }, _ => { println!("Cannot read the file"); } } } } }
運(yùn)行結(jié)果:
No such file