天天看點

Rust中的錯誤處理

作者:是啊七呀

前言

Hi,今天講一講Rust中的錯誤處理機制。在Rust中,将錯誤分為兩大類他們分别為:可恢複錯誤與不可恢複錯誤。在大部分的程式設計語言中都沒有刻意地去區分這兩種錯誤,而是通過提供異常之類的處理機制來統一處理它們。雖然Rust沒有提供類似的異常機制,但是它提供了用于可恢複錯誤的Result<T,E>枚舉類型,以及在程式出現不可恢複錯誤時中止運作的panic!宏。好了,下面就來看看Rust中給我提供的錯誤處理機制吧!

不可恢複錯誤

不可恢複錯誤往往指的是bug的另一種說法,比如嘗試通路超出數組結尾的位置等。

  1. panic宏

我們在運作代碼時總是會出現一些令我們比較無比抓狂的糟糕情情況。為了應對這樣的 場景,Rust給我門提供了一個特殊的panic! 宏。這個宏的作用是在程式會發生panic時列印 出一段錯誤提示資訊,展開并清理目前的調用棧,然後退出程式。具體使用例子如下所示:

fn main() {
    panic!("programming panic");
}
           

輸出結果如下所示:

Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/playground`
thread 'main' panicked at 'programming panic', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace           

2. 使用panic! 産生的回溯資訊

Rust通過設定RUST_BACKTRACE這個環境變量來得到回溯資訊,進而确定觸發錯誤的原因。我們可以通過這些回溯資訊中包含了内容來查找出引發錯誤的原因。具體使用如下所示:

RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/panics`
thread 'main' panicked at 'programming panic', src/main.rs:2:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
   2: panics::main
             at ./src/main.rs:2:5
   3: core::ops::function::FnOnce::call_once
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.           

可恢複錯誤

對于可恢複錯誤,比如檔案未找到等,一般需要将它們報告給使用者并再次嘗試進行操作。

  1. Result<T, E>枚舉類型
enum Result<T, E> {
    Ok(T),
    Err(E),
}
           

Rust提供了使用Result<T, E>這個枚舉類型來處理可能失敗的情況。這裡的T和E是泛型參數,T代表了OK這個變體中包含的值的類型,該變體在成功時才會傳回。而E代表了Err這個變體中包含的錯誤類型,該變體的之在失敗是才會傳回。我們用一個例子來說明一下:

use std::fs::File;
use std::io::Read; // 讀取檔案時需要用到

fn read_file(path: &str) -> Result<String, std::io::Error> { // 傳回值為String和std::io::Error類型
    let f = File::open(path);

    let mut f = match f { // 使用match表達式比對錯誤
        Ok(file) => file,
        Err(err) => return Err(err)
    };

    let mut content = String::new();

    match f.read_to_string(&mut content) { // 通過read_to_string方法将内容讀取到content這個變量中
        Ok(_) => Ok(content),
        Err(err) => Err(err),
    }
}

fn main() {
    let path = "Cargo.txt".to_string();

    println!("the path {:?}", path);

    let r = read_file(&path);

    println!("the file conetnt is {:?}", r);
}
           

運作結果如下所示:

// cd src/
// cargo run
➜  ✗ cd src 
➜  ✗ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `/home/test/Workspace/RustCoder/zhihurust/panics/target/debug/panics`
the path "Cargo.txt"
the file conetnt is Ok("[package]\nname = \"panics\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n")
           

2. unwrap和expect

unwrap和expect都是match表達式的快捷方式。它們的作用如下:

unwrap的作用是,當Result的傳回值是Ok變體時,unwrap就會傳回Ok内部的值。而當Result的傳回值是 Err變體時,unwrap則會替我們調用panic! 宏。

expect所實作的功能與unwrap是完全一樣要麼傳回OK内部的之,要麼觸發panic! 宏調用。唯一的差別就在于,expect觸發panic! 時會将傳入的參數字元串作為錯誤提示資訊輸出,而unwrap觸發的panic! 則隻會攜帶一段簡短的預設資訊。

它們的使用方式如下所示:

use std::fs::File;

fn main() {
    let path = "Cargo.txt";
    let f = File::open(path).unwrap();
    println!("the file content {:?}", f);

    let fs = File::open(path).expect("file not found!");
    println!("the file content {:?}", fs);

}
           

運作結果如下所示:

➜  src git:(master) ✗ cargo run
   Compiling panics v0.1.0 (/home/test/Workspace/RustCoder/zhihurust/panics)
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `/home/test/Workspace/RustCoder/zhihurust/panics/target/debug/panics`
the file content File { fd: 3, path: "/home/test/Workspace/RustCoder/zhihurust/panics/src/Cargo.txt", read: true, write: false }
the file content File { fd: 4, path: "/home/test/Workspace/RustCoder/zhihurust/panics/src/Cargo.txt", read: true, write: false }
           

3. ? 操作符

Rust專門提供了一個 問号運算符(?)來簡化上面例子中的文法。它的用法如下所示:

use std::fs::File;
use std::io::Read;

fn read_file2(path: &str) -> Result<String, std::io::Error> {
    let mut f = File::open(path)?;
    let mut content = String::new();
    f.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    let path = "Cargo.txt".to_string();

    let r = read_file2(&path);
    println!("the file content is {:?}", r);
}
           

通過将?放置于Result值之後,實作了與使用match 表達式來處理Result時一樣的功能。假如這個Result的值是Ok,那麼包含在Ok中的值就會作為這個表達式的結果傳回并繼續執行程式。假如這個值是Err,那麼這個值就會作為整個程式的結果傳回,如同使用了return 一樣将錯誤傳播給了調用者。

運作結果如下所示:

➜  src git:(master) ✗ cargo run
   Compiling panics v0.1.0 (/home/test/Workspace/RustCoder/zhihurust/panics)
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `/home/test/Workspace/RustCoder/zhihurust/panics/target/debug/panics`
the file content is Ok("[package]\nname = \"panics\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n")
           

4. ?運算符和from函數

from函數被定義在标準庫中的std::convert::From這個Trait中,它的作用是用于錯誤之間的轉換。被?所應用的錯誤,會隐式的被from函數處理,當?調用from函數時,它接收的錯誤類型會被轉化為目前函數傳回類型所定義的錯誤類型。from函數被用于針對不同錯誤原因,傳回同一種錯誤類型的情況,隻要每個錯誤類型實作了轉換為所傳回的錯誤類型的from函數。

5. ?鍊式調用

我們在使用?進行錯誤傳播時,我們可以使用鍊式調用的方式進行,具體使用如下所示:

use std::fs::File;
use std::io::Read;

fn read_file2(path: &str) -> Result<String, std::io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    let path = "Cargo.txt".to_string();
    let r = read_file2(&path);
    println!("the file content is {:?}", r.unwrap());
}
           

運作結果如下所示:

➜  src git:(master) ✗ cargo run
   Compiling panics v0.1.0 (/home/test/Workspace/RustCoder/zhihurust/panics)
    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `/home/test/Workspace/RustCoder/zhihurust/panics/target/debug/panics`
the file content is "[package]\nname = \"panics\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n"
           

6. ?運算符注意的點

?運算符隻能被用于傳回Result的函數,這一點就不舉例了。

小結

今天我們知道了Rust中怎麼對錯誤進行處理,并舉例了一些例子,通過這些例子我們知道了其基本用法。前面的路還很遠,繼續前進。