更友善的錯誤回報

面對錯誤的發生,我們只能接受,別無他法。而與許多其他語言相比,使用 Rust 時真的很難不察覺錯誤並應付這個現實情況:因為 Rust 沒有例外狀況,所以所有可能的錯誤狀態通常都編碼在函式的傳回類別中。

回傳結果

read_to_string 這樣的函式不會傳回字串。它傳回一個 Result,其中包含一個 String 或某種類型的錯誤 (本例為 std::io::Error)。

要如何知道錯誤類型?由於 Resultenum,可以利用 match 來檢查錯誤是哪個類型。


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { println!("Oh noes: {}", error); }
}
}

解開封裝

現在,我們已存取到檔案內容,但 match 區塊之後沒辦法對檔案內容做任何事。為了解決這一點,我們需要想辦法處理錯誤情況。難題在於 match 區塊的所有分支都需要傳回相同類型的某個結果。可是有一個簡潔的方法可以解決這個問題。


#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);
}

我們可以在 match 區塊之後使用 content 中的 String。如果 result 是錯誤,則 String 就不存在。但是由於程式會在使用 content 之前就先結束,所以這樣做沒有問題。

這看起來也許很激烈,但實際上很方便。如果程式需要讀取那個檔案,而檔案不存在時沒辦法做任何事,則結束程式是一個有效的策略。Result 中甚至有一個捷徑方法,叫做 unwrap


#![allow(unused)]
fn main() {
let content = std::fs::read_to_string("test.txt").unwrap();
}

不用驚慌

當然,中斷程式並非處理錯誤的唯一途徑。我們可以輕鬆改寫panic!,改成return

fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { return Err(error.into()); }
};
Ok(())
}

然而,這樣會變更我們的函式所需傳回的類型。其實,在我們的範例中有個隱藏的概念:這個程式碼所在的函式簽章。而這個最後的範例使用了return後,函式簽章就變得很重要。以下是完整的範例

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

我們的傳回類型是Result!這就是為什麼我們可以在第二個對應條件中寫下return Err(error);。看到底部的Ok(())了嗎?那是函式的預設傳回值,表示「Result 一切正常且沒有內容」。

問號

就像呼叫.unwrap()match的捷徑,而且錯誤條件會panic!,我們也有另一個捷徑可以使用在錯誤條件回傳returnmatch中:?

沒錯,就是問號。你可以將這個運算子附加到Result類型的值上,Rust 會將它內部擴充為類似我們剛剛編寫的match

試一下

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("file content: {}", content);
    Ok(())
}

很簡潔!

提供資訊脈絡

在你main函式中使用?時產生的錯誤訊息雖然還可以,但並不理想。例如:當你執行std::fs::read_to_string("test.txt")?但檔案test.txt不存在時,你會得到這個輸出

Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

在你的程式碼中沒有明確包含檔案名稱時,要指出哪個檔案「NotFound」會非常困難。處理這個問題的方法有很多種。

例如,我們可以建立自己的錯誤類型,然後使用它來產生自訂錯誤訊息

#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
    println!("file content: {}", content);
    Ok(())
}

現在執行這個程式碼,我們就會收到自訂錯誤訊息

Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")

雖然不太美觀,但我們可以輕鬆改寫除錯輸出以適應我們的類型。

這個模式其實很常見。儘管如此,它有一個問題:我們未儲存原始錯誤,只儲存了錯誤字串表示。常使用的 anyhow 函式庫針對此問題有絕佳的解決方案:與我們的 CustomError 類型類似,它的 Context 特質可用於加入描述。此外,它也會保留原始錯誤,因此我們會取得指向根本原因的錯誤訊息「串」。

我們先來匯入 anyhow crate,方法是在 Cargo.toml 檔案的 [dependencies] 區塊中加入 anyhow = “1.0”

完整範例如下

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("could not read file `{}`", path))?;
    println!("file content: {}", content);
    Ok(())
}

這會列印錯誤

Error: could not read file `test.txt`

Caused by:
    No such file or directory (os error 2)

整理

現在您的程式碼看起來應如下所示

use anyhow::{Context, Result};
use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() -> Result<()> {
    let args = Cli::parse();

    let content = std::fs::read_to_string(&args.path)
        .with_context(|| format!("could not read file `{}`", args.path.display()))?;

    for line in content.lines() {
        if line.contains(&args.pattern) {
            println!("{}", line);
        }
    }

    Ok(())
}