本文 AI 產出,尚未審核

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 的閉包同樣遵守所有權與借用規則,常用於 Iteratorsort_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::takestd::mem::replace

最佳實踐

  1. 盡量使用不可變借用:除非必須修改,否則以 &T 傳遞參數,讓編譯器保證資料不會在函式內被改變。
  2. 利用 ? 簡化錯誤傳遞fn foo() -> Result<T, E> { … Ok(v)?; … } 能讓錯誤自動向上層返回。
  3. 明確標註生命週期:當函式返回引用時,使用 'a 等生命週期參數避免曖昧。
  4. 避免過長的參數列表:若參數超過 3–4 個,考慮使用結構體或 enum 包裝,提升可讀性。
  5. 使用 #[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 提供了靈活的多值回傳與錯誤處理機制,讓錯誤必須被顯式處理。
  • 閉包 讓函式可以接受行為參數,配合 Iteratorsort_by 等高階 API,寫出簡潔且高效的資料流程。
  • 常見的所有權移動、借用衝突與生命週期錯誤,只要遵守「盡量借用、必要時才移動」的原則,就能在編譯階段捕捉大多數問題。

掌握以上概念後,你將能在 Rust 中寫出 安全、可維護且效能卓越 的程式碼,無論是單執行緒的演算法、CLI 工具,或是多執行緒的伺服器應用,都能得心應手。祝你在 Rust 的旅程中玩得開心,寫出更好的程式!