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_strPath 會回傳 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
})?;