iT邦幫忙

2025 iThome 鐵人賽

DAY 4
1
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 4

(Day4) Rust 所有權與借用的交錯:一個變數的歷程

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:所有權與借用的交錯:一個變數的歷程

函式參數的「身份證」:從傳遞看所有權流轉

在前面幾篇,我們理解了 Rust 不用 GC、所有權的「單身證明」,以及借用的「契約共享」的特性。

現在把這些概念結合起來,用在一個變數踏上函式調用路上時,所有權與借用是如何精準地流動的。

一開始會想說,函式傳遞參數這不是很簡單嗎?但這其實是許多程式語言中模糊且容易出錯的地帶。

在 Go 或 Python 中,函式參數的傳遞行為有時會讓人困惑:是「copy value」還是「reference」傳遞?
在 Rust 的世界裡,則用其所有權系統,在編譯時期就終結了這場混亂。

當一個變數被當作參數傳遞時,它的「身份證」——所有權——早已決定了它的命運。

三種傳遞方式:所有權、借用、複製

當我們調用一個函式時,Rust 提供了三種不同的參數傳遞方式:

  1. 所有權轉移fn(s: String) - 函式取得所有權
  2. 不可變借用fn(s: &String) - 函式只能讀取
  3. 可變借用fn(s: &mut String) - 函式可以修改

https://ithelp.ithome.com.tw/upload/images/20250918/201244620pIVAJJRIB.png

所有權轉移:fn(s: String)

fn take_ownership(s: String) {
    println!("函式內部: {}", s);
    // s 在這裡離開作用域,記憶體被釋放
}

fn main() {
    let s1 = String::from("hello");
    take_ownership(s1);  // s1 的所有權被轉移給函式
    // println!("s1: {}", s1);  // ❌ 編譯錯誤!s1 已經無效
}

所有權轉移的完整過程:

  1. s1 創建並擁有 "hello" 字串
  2. 調用 take_ownership(s1) 時,s1 的所有權被轉移給函式參數 s
  3. 函式執行完畢後,s 離開作用域,記憶體被自動釋放
  4. s1 在轉移後就失效了,不能再使用

這就像你把一本書送給朋友,從此這本書就歸朋友所有,你再也拿不回來了。

不可變借用:fn(s: &String)

fn borrow_string(s: &String) {
    println!("函式內部: {}", s);
    // s 只是借用,不能修改
    // s.push_str(" world");  // ❌ 編譯錯誤!不能修改借用的資料
}

fn main() {
    let s1 = String::from("hello");
    borrow_string(&s1);  // 傳遞借用
    println!("s1: {}", s1);  // ✅ s1 仍然有效
}

借用的優雅之處

  1. s1 創建並擁有 "hello" 字串
  2. 調用 borrow_string(&s1) 時,傳遞的是借用,不是所有權
  3. 函式可以讀取資料,但不能修改
  4. 函式執行完畢後,借用結束,s1 仍然有效

這就像你借書給朋友閱讀,朋友可以看,但不能在上面寫字,看完後書還是你的。

可變借用:fn(s: &mut String)

fn modify_string(s: &mut String) {
    s.push_str(" world");
    println!("函式內部: {}", s);
}

fn main() {
    let mut s1 = String::from("hello"); // 必須是 mut 才能建立可變借用
    modify_string(&mut s1);  // 傳遞「可變借用」
    println!("s1: {}", s1);  // ✅ s1 仍然有效,但內容已被修改為 "hello world"
}

這個例子展示了可變借用的威力:它允許函式在不取得所有權的情況下,安全地修改外部的資料。

這就像你借書給朋友,朋友可以在上面做筆記,但書的所有權還是你的。

三種傳遞方式的生命週期

https://ithelp.ithome.com.tw/upload/images/20250918/20124462rPMsH8WT5r.png

與其他語言的對比:從模糊到明確

Python:隱式的引用傳遞

在 Python 中,函式參數的傳遞行為是隱式的:

def modify_list(lst):
    lst.append(4)  # 修改原始列表
    print(f"函式內部: {lst}")

def main():
    my_list = [1, 2, 3]
    modify_list(my_list)  # 傳遞引用
    print(f"函式外部: {my_list}")  # [1, 2, 3, 4] - 被修改了!

這裡的問題是:你不知道函式是否會修改你的資料。你必須查看函式的實作才能確定。

Golang:顯式但容易出錯

Golang 透過指標讓傳遞行為變得顯式:

func modifySlice(s []int) {
    s[0] = 999  // 修改原始切片
    fmt.Printf("函式內部: %v\n", s)
}

func main() {
    mySlice := []int{1, 2, 3}
    modifySlice(mySlice)  // 傳遞切片(引用)
    fmt.Printf("函式外部: %v\n", mySlice)  // [999, 2, 3] - 被修改了!
}

雖然傳遞行為變得顯式,但 Go 無法在編譯期阻止意外的修改。

Rust:編譯期保證的明確性

Rust 的函式簽名本身就是一份完整的技術文件:

// 這個簽名告訴你:函式會取得所有權,用完就銷毀
fn take_ownership(s: String) -> usize { s.len() }

// 這個簽名告訴你:函式只會讀取,不會修改
fn read_only(s: &String) -> usize { s.len() }

// 這個簽名告訴你:函式會修改資料
fn modify_data(s: &mut String) { s.push_str(" world"); }

總結

  • 函式參數的本質:Rust 透過所有權(ownership)與借用(borrowing)在編譯期決定資料的生命週期與可變性,避免隱性副作用。
  • 三種傳遞方式:取得所有權、不可變借用、可變借用,分別對應「移動、唯讀、可改」。
  • 可預測性:僅憑簽名即可判斷呼叫方資料是否仍可用,以及是否會被修改。
  • 對比其他語言:相較 Python/Go 的隱性或寬鬆模式,Rust 以型別系統與編譯器將不確定性提前消滅。

相關連結與參考資源


上一篇
(Day3) Rust 借用 (Borrowing):有契約的共享
下一篇
(Day5) Rust Copy 與 Clone 零成本 vs 有成本複製
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言