在理解了所有權的移動 (Move) 和借用 (Borrow) 之後,我們來探討一些真實世界中更細膩的情境。
為什麼 i32
不會像 String
一樣被「移走」?
如何設計出更優雅、更高效的函式?以及,當借用規則變得複雜時,編譯器如何像一位良師益友般引導我們?
看到這裡你可能會想:難道每次傳遞一個 i32
整數給函式,原來的變數也會失效嗎?
這就引出了 Move 語義的一個重要例外:Copy
Trait。
當一個變數被傳遞時,編譯器會根據其型別是否實現 Copy
Trait 來決定採取哪種行為。
fn take_number(x: i32) {
println!("函式內部: {}", x);
}
fn main() {
let x = 5;
take_number(x); // x 的值被「複製」給函式,不是移動
println!("x: {}", x); // ✅ x 仍然有效!
}
對於實現了 Copy
的型別(如 i32
、f64
、bool
等),「=
」和函式參數傳遞都是複製,而不是移動。
為什麼這些型別可以自動複製?
因為它們的資料完全儲存在堆疊 (stack) 上,大小固定,複製的成本極低。
只是一次記憶體位元的直接拷貝 (bitwise copy)。
它們不擁有任何堆 (heap) 上的資源,所以複製它們不會產生「有兩個擁有者指向同一份堆資料」的混亂情況。
一個重要的規則是:只有當一個型別的所有成員也都是
Copy
型別時,這個型別本身才能被標記為Copy
。
這就是為什麼包含String
或Vec
的struct
不能是Copy
型別。
對於沒有實現 Copy
的型別(String
、Vec
等),它們管理著堆上的資源,複製成本較高。
因此,Rust 要求你必須手動、顯式地調用 .clone()
方法來進行深層複製。
fn take_string(s: String) {
println!("函式內部: {}", s);
}
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 顯式複製
take_string(s1); // s1 被移動
println!("s2: {}", s2); // ✅ s2 仍然有效
}
Copy
和 Clone
是 Rust 所有權系統中處理「複製」的核心概念,它們讓 API 設計更具意圖性與效率:
Copy
:無感知的廉價複製
適用於完全儲存在堆疊 (stack) 上的簡單型別,如 i32
, bool
, char
等。
複製成本極低(只是位元拷貝),因此 Rust 會自動、隱式地進行複製,而不是移動。
讓這些基礎型別的行為符合開發者直覺,無需擔心所有權轉移問題。
Clone
:有意識的昂貴複製
適用於擁有堆 (heap) 上資源的複雜型別,如 String
, Vec
。
複製意味著需要配置新的堆記憶體並拷貝資料,成本較高。
為了避免意外的效能損耗,Rust 要求必須手動、顯式地呼叫 .clone()
方法來進行深層複製。
透過區分這兩種行為,Rust 的編譯器強迫開發者在編寫程式時思考:「這裡的資料是廉價的複製,還是昂貴的複製?」
這種思考模式有助於我們設計出更高效、更安全的 API,從源頭上避免不必要的效能問題與所有權混淆。
Copy vs Clone 的區別:
特性 | Copy | Clone |
---|---|---|
觸發時機 | 自動 | 手動 |
成本 | 零成本 | 有成本 |
適用類型 | 基本類型 | 所有類型 |
語法 | let y = x; |
let y = x.clone(); |