Rust的例外處理-可復原類型的錯誤

Rust Recoverable Error Handling

July 18, 2024, 3:21 p.m.
程式語言

跟Python不一樣的地方在於,Rust沒有try-catch結構的例外處理機制,取而代之的是將錯誤分成兩種類型:
- 可復原的錯誤
- 不可復原的錯誤
然後針對這兩種錯誤進行不同的處理方式。

可復原的錯誤 (Recoverable)

沒有嚴重到要讓整個程序停止的錯誤。
例如:試圖開啟不存在的檔案路徑

Rust提供了Result這個型別處理這種類型的錯誤:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • T表示成功的時候會回傳的型別
  • E表示失敗時的回傳型別
    透過這些泛型參數,就可以將Result用在不同的場合來處理成功與失敗的結果後,回傳不相同的型別。

舉例來說:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}
  • 假設hello.txt存在,greeting_file_result就會是File Handle
  • 不存在的話,就會是包含該錯誤資訊的ErrInstance

因此,我們需要在外面額外撰寫不同的處理邏輯,針對不同的結果採取不同的動作:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("開啟檔案時發生問題:{:?}", error),
    };
}

利用match表達式,我們可以針對Ok或是Err變體做不同的處理。

配對不同的錯誤

File::openErr變體的回傳型別為io::Error,這是標準函式庫提供的結構體。這個結構有一個kind方法,可以讓我們取得io::ErrorKind的數值,裡面包含了各種io過程可能發生的錯誤類型,例如ErrorKind::NotFound

我們可以用繼續用match去針對不同的io類型錯誤做客製化的處理。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("建立檔案時發生問題:{:?}", e),
            },
            other_error => {
                panic!("開啟檔案時發生問題:{:?}", other_error);
            }
        },
    };
}

把邏輯改為:
- 試圖打開hello.txt
- 要是打開失敗,而且錯誤是NotFound就建立新的檔案
- 建立的過程也有可能失敗,所以再match一次錯誤
- 成功的話回傳file handle
- 要是繼續出錯,直接Panic
- 其他錯誤就直接panic!

Match以外的選擇,使用Closure配對Result<T, E>

match表達式有可能隨著需求變多,導致程式碼囉唆起來。這個時候我們可以用標準函式庫提供的unwrap_or_else搭配Closure的功能,讓程式碼便的簡潔不少。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("建立檔案時發生問題:{:?}", error);
            })
        } else {
            panic!("開啟檔案時發生問題:{:?}", error);
        }
    });
}

Match以外的選擇,unwrap與expect

Result<T ,E>型別本身就有非常多的輔助方法來執行不同的特定任務,例如unwrap()

Result.unwrap()

如果Result的結果為Okunwrap就回傳Ok裡面的數值。如果是Errunwrap就會呼叫panic

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

Result.expect()

expectunwrap類似,但是他的目的著重在panic!時的處理。
使用expect可以讓你在發生panic!的時候,提供更完善的錯誤訊息,例如:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt 應該要存在此專案中");
}

官方文件特地表明了,通常在正式環境的CodeBase中,多數人會使用expect讓出錯時可以有更多資訊查看,而非使用unwrap

Propagating傳遞錯誤

有時候你可能不會想要直接在函數內處理錯誤,而是會想把錯誤往外拋出去給呼叫的程式碼處理,這樣的行為稱之為Propagating

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}
  • 跟上面的範例不同,我們把panic的部分都拿掉了
  • 中間可以看到username_file_result的處理過程中如果碰到錯誤,會直間返回Err(e)

?運算子

因為中途使用reutrn Err(e)的情境因為很常見,所以Rust提供了?運算子來簡化了這段的流程:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}
  • ?運算符直接取代了原本的match, Ok, Err(e)
  • File::open出問題的話,就會直接使用return關鍵字回傳Err

另外這段程式碼還可以更簡潔的串連再一起:

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

?運算子的限制

只能用在函數回傳值相容於?使用的數值才行

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}
  • main函數的回傳值是()
  • ?有可能會直接return Err,也就是說回傳值的部分跟main會不匹配

錯誤訊息:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error

  • 編譯器告訴我們「只能在回傳型別為 Result 或 Option 或其他有實作 FromResidual 的型別的函式才能使用 ? 運算子」

參考資料

Tags:

Rust