Rust 課程 – 函數與控制流
主題:表達式 vs 陳述式
簡介
在 Rust 中,表達式 (expression) 與 陳述式 (statement) 的差異是語言設計的核心之一。它直接影響程式的可讀性、可組合性以及錯誤處理的方式。對於剛接觸 Rust 的學習者來說,往往會把兩者混為一談,結果寫出來的程式碼既冗長又易出錯。
本篇文章將以 淺顯易懂 的方式說明什麼是表達式、什麼是陳述式,並透過實作範例展示它們在 函式、條件判斷、迴圈 等控制流結構中的實務運用。掌握這個概念後,你將能寫出更具 表達力、更安全且更符合 Rust 風格的程式碼。
核心概念
1. 基本定義
| 項目 | 說明 |
|---|---|
| 表達式 | 產生一個值,且在語法上可以被「放」在其他表達式裡。例如 5 + 3、some_vec.len()、if condition { 1 } else { 0 }。 |
| 陳述式 | 執行某個動作,但不會直接產生可用的值。常見的有變數宣告、let、return、loop、while、for、break、continue 等。 |
重點:在 Rust 中,幾乎所有程式碼都是 表達式,只有少數關鍵字會形成 陳述式。這讓語言在編譯期就能進行更嚴格的型別推斷與所有權檢查。
2. 為什麼表達式很重要?
- 函式回傳值:Rust 的函式最後一行如果是表達式,就會自動成為回傳值,省去
return關鍵字。 - 組合子程式:表達式可以直接嵌套在其他表達式裡,形成簡潔的「鏈式」寫法。
- 所有權與借用:表達式的結果會遵循所有權規則,讓編譯器在編譯階段就捕捉到潛在的錯誤。
3. 常見的表達式類型
| 類型 | 範例 | 說明 |
|---|---|---|
| 字面值 | 42、true、"hello" |
直接產生固定值。 |
| 運算子表達式 | a + b * c |
依照運算子優先權計算後產生值。 |
| 函式呼叫 | std::mem::size_of::<i32>() |
呼叫函式後取得回傳值。 |
| 區塊表達式 | { let x = 5; x * 2 } |
大括號內的最後一個表達式成為區塊的結果。 |
| 條件表達式 | if cond { 1 } else { 0 } |
if/else 可以是表達式,必須保證兩支分支回傳同型別。 |
| 迭代表達式 | `vec.iter().map( | x |
4. 陳述式的角色
- 變數綁定:
let x = 5;(宣告並綁定) - 流程控制:
if,match,loop,while,for(雖然if/match也能是表達式,但作為控制流時屬於陳述式) - 返回:
return expr;(強制提前返回) - 宏展開:
println!("Hello");(宏本身是陳述式)
提示:在需要「副作用」而非「值」的情境下,使用陳述式會讓意圖更清晰。
程式碼範例
以下示範 5 個常見情境,說明表達式與陳述式的差異與最佳寫法。
範例 1:函式回傳值的表達式寫法
// 使用最後一行的表達式自動回傳
fn max(a: i32, b: i32) -> i32 {
if a > b { a } else { b } // <-- 這裡是表達式,直接成為回傳值
}
說明:
if這裡是 表達式,兩個分支都回傳i32,因此函式不需要return。
範例 2:區塊表達式與所有權
fn create_string() -> String {
// 大括號內是區塊表達式,最後一行的 String 會被移動 (move) 出去
{
let s = String::from("Rust");
s // 這裡是表達式,返回 s 的所有權
}
}
說明:區塊本身是表達式,裡面的
let是陳述式。最後一行s把所有權交給呼叫端。
範例 3:使用 match 作為表達式
enum Shape {
Circle(f64),
Rectangle { w: f64, h: f64 },
}
fn area(shape: Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle { w, h } => w * h,
} // `match` 產生 f64,直接回傳
}
說明:
match每個分支都回傳同型別 (f64),因此整個match成為表達式。
範例 4:迴圈中的 break 帶值(表達式)
fn find_first_even(nums: &[i32]) -> Option<i32> {
for &n in nums {
if n % 2 == 0 {
break Some(n); // `break` 可以帶值,成為迴圈的結果表達式
}
}
None
}
說明:
break Some(n)把Some(n)作為迴圈的回傳值,for迴圈本身是表達式。
範例 5:從陳述式到表達式的改寫
// 陳述式寫法
fn sum_until(limit: i32) -> i32 {
let mut sum = 0;
let mut i = 0;
while i < limit {
sum += i;
i += 1;
}
sum // 最後的 `sum` 是表達式,作為回傳值
}
// 更 idiomatic 的表達式寫法
fn sum_until(limit: i32) -> i32 {
(0..limit).fold(0, |acc, x| acc + x) // 完全使用表達式
}
說明:第二個版本把
while迴圈(陳述式)改寫成fold(純表達式),更符合 Rust 的函式式風格。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 建議的解決方式 |
|---|---|---|
| 忘記在區塊最後加分號 | 區塊會返回最後一行的值,導致型別不符合預期 | 若不想回傳值,在最後一行加分號 (;) 讓它變成陳述式 |
if / match 分支型別不一致 |
編譯錯誤 mismatched types |
確保所有分支回傳同一型別,或使用 () 作為單位型別 |
| 在需要副作用的地方使用表達式 | 程式可讀性下降,容易忽略副作用 | 明確使用陳述式(如 let _ = expr;)或加入註解說明 |
過度使用 return |
失去 Rust 「最後一行即回傳」的簡潔性 | 盡量讓函式最後一行成為表達式,除非需要提前返回 |
在 while/for 內直接 break 帶值 |
初學者可能不熟悉此語法 | 先熟悉基本迴圈,再逐步使用 break value 來返回結果 |
最佳實踐:
- 優先使用表達式:讓程式碼更具宣告性與可組合性。
- 保持型別一致:尤其在
if、match、loop等表達式中。 - 利用區塊表達式:在需要臨時變數但又想回傳結果時,使用
{ … }包住程式碼。 - 適度加入註解:對於看起來不直觀的表達式(例如
break Some(v)),加上說明可提升可讀性。
實際應用場景
1. 配置檔解析
在解析 JSON、YAML 等設定檔時,常會根據不同欄位組合出不同的結構。利用 match 表達式可以一次完成 檢查 + 建構,避免多層 if 陳述式。
fn parse_mode(mode: &str) -> Result<Mode, &'static str> {
match mode {
"debug" => Ok(Mode::Debug),
"release" => Ok(Mode::Release),
_ => Err("未知的模式"),
}
}
2. 錯誤處理(Result)的鏈式寫法
Result 本身是表達式,配合 ? 操作子可讓錯誤傳遞變得非常簡潔。
fn read_file(path: &str) -> std::io::Result<String> {
let mut file = std::fs::File::open(path)?; // 陳述式,返回 Result
let mut contents = String::new();
file.read_to_string(&mut contents)?; // 仍是陳述式
Ok(contents) // 表達式,作為回傳值
}
3. 生成測試資料
在單元測試中,常會使用 區塊表達式 產生臨時資料,同時保證所有權正確。
#[test]
fn test_sum() {
let data = {
let mut v = Vec::new();
for i in 1..=5 { v.push(i); }
v // 這裡返回 Vec<i32>
};
assert_eq!(data.iter().sum::<i32>(), 15);
}
總結
- 表達式 產生值,可嵌入其他表達式;陳述式 執行動作,通常不返回值。
- Rust 的設計讓 大部分程式碼都是表達式,這使得函式、控制流、所有權檢查都能在編譯期得到嚴格保證。
- 熟練 區塊表達式、
if/match作為表達式、break帶值等技巧,能寫出更簡潔、可讀且安全的程式。 - 在實務開發中,將配置解析、錯誤處理、測試資料生成等情境轉換為表達式風格,能顯著提升程式碼品質與維護效率。
掌握了「表達式 vs 陳述式」的差異與使用時機,你就能在 Rust 中更自然地運用 函式式編程 的優雅,同時保持 系統層級的安全性。祝你在 Rust 的旅程中寫出更好、更安全的程式碼!