Rust 課程 – 函數與控制流
主題:參數與回傳值
簡介
在任何程式語言裡,**函式(function)**都是組織程式碼、重複使用邏輯的核心工具。Rust 以「安全」與「零成本抽象」為設計哲學,對函式的參數與回傳值也提供了嚴謹的型別檢查與所有權規則。掌握這些概念不僅能寫出可讀性高的程式,還能避免常見的記憶體安全問題,讓程式在編譯階段就捕捉到潛在錯誤。
本篇文章針對 參數傳遞方式、回傳值的表達 以及 所有權、借用與生命週期 在函式中的互動進行說明。透過實作範例,你將能快速上手,並在日後的專案中正確運用 Rust 的函式特性。
核心概念
1. 基本函式宣告與呼叫
fn add(a: i32, b: i32) -> i32 {
a + b // 最後一行是隱式回傳值
}
fn為關鍵字,後接函式名稱add。- 參數列表必須明確寫出 型別 (
i32)。 -> i32表示回傳值的型別;若沒有回傳值,可省略或寫成-> ()(單位類型)。- 省略
return,最後一個表達式自動成為回傳值;若使用return,則必須在行尾加分號。
let sum = add(3, 5);
println!("3 + 5 = {}", sum);
2. 參數傳遞方式:所有權、借用與複製
| 方式 | 說明 | 範例 |
|---|---|---|
| 值傳遞(move) | 參數取得所有權,呼叫端的變數在此之後不可再使用。 | fn consume(s: String) {} |
| 借用(reference) | 以 & 或 &mut 取得資料的暫時存取權,所有權不變。 |
fn borrow(s: &String) {} |
| 複製(Copy) | 內建的 Copy 類型(如整數、布林)在傳遞時會自動複製,無所有權移動。 |
fn copy_i32(x: i32) {} |
2‑1. 移動(move)範例
fn take_ownership(s: String) {
println!("Got: {}", s);
// s 在此離開作用域後會被 drop
}
fn main() {
let msg = String::from("Hello, Rust!");
take_ownership(msg);
// println!("{}", msg); // ❌ 編譯錯誤:msg 已被移動
}
2‑2. 不可變借用(immutable borrow)
fn read_len(s: &String) -> usize {
s.len() // 只讀取,不改變內容
}
fn main() {
let text = String::from("Rust");
let len = read_len(&text); // 使用 & 取得借用
println!("Length = {}", len);
// 此時仍可繼續使用 text
println!("Original = {}", text);
}
2‑3. 可變借用(mutable borrow)
fn append_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let mut greeting = String::from("Hi");
append_exclamation(&mut greeting);
println!("{}", greeting); // => "Hi!"
}
注意:同一時間只能有 一個可變借用 或 任意數量的不可變借用,這是 Rust 防止資料競爭的核心規則。
3. 多重回傳值:元組與 Result
Rust 沒有「多值回傳」的語法,但可以透過 元組 或 結構體 包裝多個值。常見的錯誤處理則使用 Result<T, E>。
3‑1. 使用元組回傳
fn divide(dividend: f64, divisor: f64) -> (f64, bool) {
if divisor == 0.0 {
(0.0, false) // 第二個元素表示是否成功
} else {
(dividend / divisor, true)
}
}
fn main() {
let (quot, ok) = divide(10.0, 2.0);
if ok {
println!("Result = {}", quot);
}
}
3‑2. 使用 Result 進行錯誤傳遞
fn safe_divide(dividend: f64, divisor: f64) -> Result<f64, &'static str> {
if divisor == 0.0 {
Err("除數不能為 0")
} else {
Ok(dividend / divisor)
}
}
fn main() {
match safe_divide(10.0, 0.0) {
Ok(v) => println!("Quotient = {}", v),
Err(e) => eprintln!("Error: {}", e),
}
}
Result 讓錯誤資訊必須被顯式處理,避免了「忽略錯誤」的隱憂。
4. 高階函式:閉包(Closure)作為參數
Rust 的閉包同樣遵守所有權與借用規則,常用於 Iterator、sort_by 等高階函式。
fn apply<F>(x: i32, f: F) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
fn main() {
let double = |n| n * 2; // 捕獲環境的閉包
let result = apply(5, double);
println!("5 doubled = {}", result);
}
技巧:若閉包只讀取外部變數,編譯器會自動推導為
Fn;若需要修改外部變數,則使用FnMut;若要取得所有權,則使用FnOnce。
常見陷阱與最佳實踐
| 陷阱 | 可能的錯誤 | 建議的解決方式 |
|---|---|---|
| 所有權被意外移動 | 呼叫函式後變數無法再使用,編譯錯誤 | 若只需要讀取,改用 &T 或 &mut T;若需要保留所有權,可在參數前加 ref(模式匹配) |
| 同時存在可變與不可變借用 | cannot borrow ... as mutable because it is also borrowed as immutable |
確認借用的生命週期,使用更小的作用域或 std::mem::replace 重新取得所有權 |
| 返回引用卻指向局部變數 | returns a reference to data owned by the current function |
使用 'static 常量、傳入的參數或 Box/Arc 之類的 heap 資料結構 |
忘記處理 Result |
編譯警告或程式在執行時 panic | 使用 ? 運算子向上層傳遞錯誤,或明確 match/unwrap_or_else 處理 |
過度使用 clone() |
造成不必要的記憶體拷貝 | 先評估是否能改為借用;若真的需要所有權,考慮 std::mem::take 或 std::mem::replace |
最佳實踐
- 盡量使用不可變借用:除非必須修改,否則以
&T傳遞參數,讓編譯器保證資料不會在函式內被改變。 - 利用
?簡化錯誤傳遞:fn foo() -> Result<T, E> { … Ok(v)?; … }能讓錯誤自動向上層返回。 - 明確標註生命週期:當函式返回引用時,使用
'a等生命週期參數避免曖昧。 - 避免過長的參數列表:若參數超過 3–4 個,考慮使用結構體或
enum包裝,提升可讀性。 - 使用
#[must_use]:對重要回傳值加上此屬性,提醒使用者必須處理結果。
實際應用場景
1. 資料驗證與錯誤回報
在 Web API 或 CLI 工具中,常需要驗證使用者輸入並回傳錯誤訊息。利用 Result<T, E> 搭配自訂錯誤型別,可讓錯誤資訊在編譯期即被強制處理。
#[derive(Debug)]
enum ParseError {
Empty,
InvalidChar(char),
}
fn parse_digit(s: &str) -> Result<u8, ParseError> {
let ch = s.chars().next().ok_or(ParseError::Empty)?;
if ch.is_ascii_digit() {
Ok(ch.to_digit(10).unwrap() as u8)
} else {
Err(ParseError::InvalidChar(ch))
}
}
2. 高效的資料處理流水線(Iterator)
Rust 的 Iterator 需要傳入閉包作為 過濾、映射、折疊 等操作。正確的參數借用方式能避免不必要的資料搬移,提升效能。
fn filter_even(nums: &[i32]) -> Vec<i32> {
nums.iter()
.filter(|&&x| x % 2 == 0) // 只讀取,不搬移
.cloned() // 複製 i32 (Copy) 成新 Vec
.collect()
}
3. 多執行緒共享資源
在 std::thread 中傳遞資料時,若要讓多個執行緒共享同一筆資料,必須使用 Arc<T>(原子參考計數)與 Mutex<T>(互斥鎖),而函式的參數型別則變成 Arc<Mutex<T>>。
use std::sync::{Arc, Mutex};
use std::thread;
fn increment(counter: Arc<Mutex<i32>>) {
let mut num = counter.lock().unwrap();
*num += 1;
}
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || increment(c)));
}
for h in handles {
h.join().unwrap();
}
println!("Final count = {}", *counter.lock().unwrap());
}
總結
- 函式的參數與回傳值是 Rust 程式設計的基石,必須明確標示型別、所有權與生命週期。
- 透過 值傳遞、不可變借用、可變借用 三種方式,你可以在安全與效能之間取得平衡。
- 元組、結構體與
Result提供了靈活的多值回傳與錯誤處理機制,讓錯誤必須被顯式處理。 - 閉包 讓函式可以接受行為參數,配合
Iterator、sort_by等高階 API,寫出簡潔且高效的資料流程。 - 常見的所有權移動、借用衝突與生命週期錯誤,只要遵守「盡量借用、必要時才移動」的原則,就能在編譯階段捕捉大多數問題。
掌握以上概念後,你將能在 Rust 中寫出 安全、可維護且效能卓越 的程式碼,無論是單執行緒的演算法、CLI 工具,或是多執行緒的伺服器應用,都能得心應手。祝你在 Rust 的旅程中玩得開心,寫出更好的程式!