在前面幾篇,我們理解了 Rust 不用 GC、所有權的「單身證明」,以及借用的「契約共享」的特性。
現在把這些概念結合起來,用在一個變數踏上函式調用路上時,所有權與借用是如何精準地流動的。
一開始會想說,函式傳遞參數這不是很簡單嗎?但這其實是許多程式語言中模糊且容易出錯的地帶。
在 Go 或 Python 中,函式參數的傳遞行為有時會讓人困惑:是「copy value」還是「reference」傳遞?
在 Rust 的世界裡,則用其所有權系統,在編譯時期就終結了這場混亂。
當一個變數被當作參數傳遞時,它的「身份證」——所有權——早已決定了它的命運。
當我們調用一個函式時,Rust 提供了三種不同的參數傳遞方式:
fn(s: String)
- 函式取得所有權fn(s: &String)
- 函式只能讀取fn(s: &mut String)
- 函式可以修改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 已經無效
}
所有權轉移的完整過程:
s1
創建並擁有 "hello" 字串take_ownership(s1)
時,s1
的所有權被轉移給函式參數 s
s
離開作用域,記憶體被自動釋放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 仍然有效
}
借用的優雅之處
s1
創建並擁有 "hello" 字串borrow_string(&s1)
時,傳遞的是借用,不是所有權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"
}
這個例子展示了可變借用的威力:它允許函式在不取得所有權的情況下,安全地修改外部的資料。
這就像你借書給朋友,朋友可以在上面做筆記,但書的所有權還是你的。
在 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 透過指標讓傳遞行為變得顯式:
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 的函式簽名本身就是一份完整的技術文件:
// 這個簽名告訴你:函式會取得所有權,用完就銷毀
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"); }