合適的回饋錯誤
我們都無能為力,只能接受會發生錯誤的事實。
與許多其他語言相比,很難不去注意和應對這個現實。
在使用 Rust 時:既然沒有例外,所有可能的錯誤狀態通常都編碼在函數的傳回型別中。
結果
像 read_to_string 這樣的函數不會回傳字串。
相反的,它會傳回一個 Result,裡面包含一個“字串”或某種其它型別的錯誤
(在本例子為std::io::Error)。
那麼如何得知是哪一種型別呢?
因為 Result 也是 enum 型別,
可以使用 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。
如果 result 是個錯誤, 字串就不存在。
但好家在,程式會在我們使用 content 之前就會自行退出了。
這種做法看起來有些極端,卻是十分實用的。
如果你的程式需要讀取一個文件, 並且在文件不存在時無法執行任何操作,那麼退出是十分合理、有效的選擇。
在 Result 中還有一個快捷方法,叫做 unwrap:
#![allow(unused)] fn main() { let content = std::fs::read_to_string("test.txt").unwrap(); }
無須 panic
當然,退出程式並非處理錯誤的唯一辦法。
除 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!
這也就是為什麼我們可以在 match 的第二個分支寫 return Err(error);。
看到最下面的 Ok(()) 是什麼嗎?
它是函數的預設回傳值, 意思為「結果沒問題,沒有內容」。
問題(!)標記
如同呼叫 .unwrap() 相當於 match 中快捷設定錯誤分支中 panic!,
我們還有另一個快速的呼叫使得在 match 的錯誤分支中直接回傳: ? 。
是的,就是這個問號(?)。
你可以在 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 特徵可用於新增描述。
此外,它還保留了原始錯誤,
因此我們會得到一串( chain )錯誤訊息,同時指出根本原因。
讓我們先在 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(())
}