Rust 的語法相較獨特,許多人在剛開始學習理解 Rust 程式碼的時候可能會有點納悶。
fn formula(mut a: f32, b: f32) -> f32 {
if a > 1.0 {
return a + b;
}
else if b > 1.0 {
return a - b;
}
a += 3.0;
a * b
}
容易讓 C/C++ 工程師困惑的是 formula function 的最後一行,為何不用寫為 return a * b;。那是因為 rust 主要是一個 expression language,而 expression 會產生值。
return a * b; 與 a * b 都有同樣的效果,但 Rust 工程師會更傾向寫為後者。那麼 statement 又是什麼?根據 Rust 的定義,statement 有兩種:declaration statements 與 expression statements。declaration statements 例如 let 與宣告物件(item),expression statements 則是對 expression 進行估值(evaluate),但忽略結果。
當然在前一篇文章我們知道了如何將 formula 簡化成 match。
再來是 unwrap 與 ?。我相信不少 C/C++ 工程師都會自己定義一些 macro 用來處理一長串的錯誤處理的流程控制,例如:
#define check(exp, err) {if(FAILED(exp)){assert(err, exp); return false;}}
當然隨著專案發展我們會遇到更多更複雜的變體,很難單純用單一 macro 概括,所以不少人會用統一的 error code 或 Error 物件來避免錯誤處理的程式碼不斷重複影響閱讀性。rust 明顯有一套統一的處理流程。
pub type Result<T> = std::result::Result<T, Diagnostic>;
pub fn load_file<'a>(filepath: &'a Path) -> Result<SourceFile> {
let s = std::fs::read_to_string(filepath).map_err(|_| {
let pathstr = filepath.to_str().unwrap();
let diag = Diagnostic {
message: format!("failed to load file {}", pathstr),
};
diag
})?;
let file = SourceFile::new(s);
Ok(file)
}
首先來看看 unwrap
let pathstr = filepath.to_str().unwrap();
to_str 是 Path 會回傳 Option<&str> 的一個 method,就像是把結果型別包覆(wrap)起來一樣,Option 提供了一個介面 unwrap 供我們把具值狀況下真正實際的數值取出來。問題是:如果 filepath.to_str() 的結果真的沒有數值(None),那麼 unwrap 的結果又會如何?unwrap 的實作其實就是一個再簡單不過的match,查看原始碼我們得知程式會直接 panic。
pub const fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
因此 unwrap 相當於我們以往寫程式「如果真的沒有數值,那它肯定是一個exception」(而非error)的態度。
有了 unwrap 的基礎我們就能接著學習 question mark operator,也就是 ?。
let s = std::fs::read_to_string(filepath)?;
當 std::fs::read_to_string 的結果為 Err 時,會直接回傳 Err(From::from(e)),反之則 unwrap。question mark operator 的設計大幅簡化傳統 if(!succeed()) return error;的流程,並統一 Rust 程式大部分的開發習慣。
回頭查看 load_file 我們會發現回傳型別是 std::result::Result<T, Diagnostic>,這時就與標準函式庫的 std::io::Result 有所不同,我們必須要將 std::io::Error 轉換成 Diagnostic。確實我們可以寫一個 match 來進行轉換,不過 Rust 內建提供一個更簡單的辦法,那就是 map_err,顧名思義會以開發者傳入的 function 將原始的錯誤數值轉換成其他型別。
let s = std::fs::read_to_string(filepath).map_err(|_| {
let pathstr = filepath.to_str().unwrap();
let diag = Diagnostic {
message: format!("failed to load file {}", pathstr),
};
diag
})?;