合適的回饋錯誤
我們都無能為力,只能接受會發生錯誤的事實。
與許多其他語言相比,很難不去注意和應對這個現實。
在使用 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(())
}