Rust 有一套獨特的處理異常情況的機制,它并不像其它語言中的 try 機制那樣簡單。
首先,程序中一般會出現(xiàn)兩種錯誤:可恢復錯誤和不可恢復錯誤。
可恢復錯誤的典型案例是文件訪問錯誤,如果訪問一個文件失敗,有可能是因為它正在被占用,是正常的,我們可以通過等待來解決。
但還有一種錯誤是由編程中無法解決的邏輯錯誤導致的,例如訪問數(shù)組末尾以外的位置。
大多數(shù)編程語言不區(qū)分這兩種錯誤,并用 Exception (異常)類來表示錯誤。在 Rust 中沒有 Exception。
對于可恢復錯誤用 Result<T, E> 類來處理,對于不可恢復錯誤使用 panic! 宏來處理。
本章以前沒有專門介紹 Rust 宏的語法,但已經(jīng)使用過了 println! 宏,因為這些宏的使用較為簡單,所以暫時不需要徹底掌握它,我們可以用同樣的方法先學會使用 panic! 宏的使用方法。
fn main() {
panic!("error occured");
println!("Hello, Rust");
}運行結(jié)果:
thread 'main' panicked at 'error occured', src\main.rs:3:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
很顯然,程序并不能如約運行到 println!("Hello, Rust") ,而是在 panic! 宏被調(diào)用時停止了運行。
不可恢復的錯誤一定會導致程序受到致命的打擊而終止運行。
讓我們注視錯誤輸出的兩行:
第一行輸出了 panic! 宏調(diào)用的位置以及其輸出的錯誤信息。
第二行是一句提示,翻譯成中文就是"通過 `RUST_BACKTRACE=1` 環(huán)境變量運行以顯示回溯"。接下來我們將介紹回溯(backtrace)。
緊接著剛才的實例,我們在 VSCode 中新建一個終端:

在新建的終端里設置環(huán)境變量(不同的終端方法不同,這里介紹兩種主要的方法):
如果在 Windows 7 及以上的 Windows 系統(tǒng)版本中,默認使用的終端命令行是 Powershell,請使用以下命令:
$env:RUST_BACKTRACE=1 ; cargo run
如果你使用的是 Linux 或 macOS 等 UNIX 系統(tǒng),一般情況下默認使用的是 bash 命令行,請使用以下命令:
RUST_BACKTRACE=1 cargo run
然后,你會看到以下文字:
thread 'main' panicked at 'error occured', src\main.rs:3:5 stack backtrace: ... 11: greeting::main at .\src\main.rs:3 ...
回溯是不可恢復錯誤的另一種處理方式,它會展開運行的棧并輸出所有的信息,然后程序依然會退出。上面的省略號省略了大量的輸出信息,我們可以找到我們編寫的 panic! 宏觸發(fā)的錯誤。
此概念十分類似于 Java 編程語言中的異常。實際上在 C 語言中我們就常常將函數(shù)返回值設置成整數(shù)來表達函數(shù)遇到的錯誤,在 Rust 中通過 Result<T, E> 枚舉類作返回值來進行異常表達:
enum Result<T, E> {
Ok(T),
Err(E),
}在 Rust 標準庫中可能產(chǎn)生異常的函數(shù)的返回值都是 Result 類型的。例如:當我們嘗試打開一個文件時:
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 文件不存在,會打印 "Failed to open the file."。
當然,我們在枚舉類章節(jié)講到的 if let 語法可以簡化 match 語法塊:
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.");
}
}如果想使一個可恢復錯誤按不可恢復錯誤處理,Result 類提供了兩個辦法: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.");
}這段程序相當于在 Result 為 Err 時調(diào)用 panic! 宏。兩者的區(qū)別在于 expect 能夠向 panic! 宏發(fā)送一段指定的錯誤信息。
之前所講的是接收到錯誤的處理方式,但是如果我們自己編寫一個函數(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");
}
}運行結(jié)果:
Ok: f(-1) = 10000
這段程序中函數(shù) f 是錯誤的根源,現(xiàn)在我們再寫一個傳遞錯誤的函數(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)的錯誤(這里的 g 只是一個簡單的實例,實際上傳遞錯誤的函數(shù)一般還包含很多其它操作)。
這樣寫有些冗長,Rust 中可以在 Result 對象后添加 ? 操作符將同類的 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) // 因為確定 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");
}
}運行結(jié)果:
Ok: g(10000) = 10000
? 符的實際作用是將 Result 類非異常的值直接取出,如果有異常就將異常 Result 返回出去。所以,? 符僅用于返回值類型為 Result<T, E> 的函數(shù),其中 E 類型必須和 ? 所處理的 Result 的 E 類型一致。
到此為止,Rust 似乎沒有像 try 塊一樣可以令任何位置發(fā)生的同類異常都直接得到相同的解決的語法,但這樣并不意味著 Rust 實現(xiàn)不了:我們完全可以把 try 塊在獨立的函數(shù)中實現(xiàn),將所有的異常都傳遞出去解決。實際上這才是一個分化良好的程序應當遵循的編程方法:應該注重獨立功能的完整性。
但是這樣需要判斷 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");
}
}
}
}
}運行結(jié)果:
No such file