Vec 與所有權
本集目標
理解 Vec 的所有權行為,以及它和 String / &str 的對稱關係。
概念說明
Vec 和 String 是一對
在前面幾集,我們學了 String 和 &str 的關係:
| 擁有版本 | 借用版本 |
|---|---|
String | &str |
Vec 也有完全一樣的對應:
| 擁有版本 | 借用版本 |
|---|---|
Vec | &[T](切片) |
String 擁有一段文字,&str 借用一段文字。Vec 擁有一組元素,&[T] 借用一組元素。概念完全對稱。
Vec 會 move
Vec 的資料主要在 heap 上,所以它不是 Copy。賦值和傳入函數都會 move:
fn main() {
let v1 = vec![1, 2, 3];
let v2 = v1; // move!v1 不能再用了
}
跟 String 一模一樣。
函數參數用切片 &[T]
跟 String / &str 的建議一樣——如果函數只需要讀取 i32 的 Vec 的內容,用切片 &[i32]:
fn sum(nums: &[i32]) -> i32 {
let mut total = 0;
for x in nums {
total += x;
}
total
}
fn main() {
let v = vec![1, 2, 3, 4, 5];
let total = sum(&v); // i32 的 &Vec 自動轉成 &[i32]
println!("總和:{}", total);
println!("v 還在:{:?}", v);
}
就像 &String 會自動轉成 &str,i32 的 &Vec 也會自動轉成 &[i32]。
for 迴圈與所有權
這是很重要的一點:for 迴圈走訪 Vec 時,可以選擇要 move 還是 borrow:
for x in v——move!
fn main() {
let v = vec![1, 2, 3];
for x in v {
println!("{}", x);
}
// v 被 move 走了,不能再用!
}
for x in v 會消耗整個 Vec。迴圈結束後,v 就不存在了。
for x in &v——borrow!
fn main() {
let v = vec![1, 2, 3];
for x in &v {
println!("{}", x); // x 的型別是 &i32
}
println!("v 還在:{:?}", v); // OK!
}
for x in &v 只是借用,v 不會被消耗。
這裡要特別注意:x 不是 i32,而是參考,型別是 &i32。因為迴圈走訪的是借來的 &v,所以每次拿到的元素也只是借來的,不會把元素移出 Vec。同樣地,前面函數參數的 nums: &[i32] 本身已經是借來的切片,所以 for x in nums 裡的 x 也是 &i32。
大部分情況你應該用 for x in &v,除非你確定不再需要這個 Vec。
範例程式碼
// 參數用切片:i32 的 &Vec 自動轉 &[i32]
fn sum(nums: &[i32]) -> i32 {
let mut total = 0;
for x in nums {
total += x;
}
total
}
fn print_all(nums: &[i32]) {
let mut first = true;
for x in nums {
if first {
first = false;
} else {
print!(", ");
}
print!("{}", x);
}
println!();
}
fn main() {
// Vec 會 move
let v1 = vec![10, 20, 30];
let v2 = v1.clone(); // clone 保留 v1
println!("v1 = {:?}", v1);
println!("v2 = {:?}", v2);
// 函數用切片參數(借用)
let scores = vec![85, 92, 78, 95, 88];
println!("總分 = {}", sum(&scores));
print_all(&scores);
println!("scores 還在:{:?}", scores);
// 切片操作
let slice = &scores[1..4]; // 借用一部分
println!("中間三筆:{:?}", slice);
println!("中間三筆的總分 = {}", sum(slice));
// for x in &v:借用走訪
println!("逐一列出(借用):");
for s in &scores {
println!(" {}", s);
}
println!("scores 還在:{:?}", scores);
// for x in v:move 走訪(用完就沒了)
let temp = vec![1, 2, 3];
println!("消耗走訪:");
for x in temp {
println!(" {}", x);
}
// temp 已經被 move 了,下面會編譯錯誤:
// println!("{:?}", temp);
// 對稱關係整理
// String ↔ &str (擁有 ↔ 借用 文字)
// Vec ↔ &[T] (擁有 ↔ 借用 一組值)
println!("--- 對稱關係 ---");
let s = String::from("hello");
let s_ref: &str = &s; // &String → &str
println!("String: {}, &str: {}", s, s_ref);
let v = vec![1, 2, 3];
let v_ref: &[i32] = &v; // i32 的 &Vec → &[i32]
println!("Vec: {:?}, slice: {:?}", v, v_ref);
}
「裝 i32 的 Vec」的型別怎麼寫?
這一集我們一直說「i32 的 Vec」,但你可能已經注意到:程式碼裡從來沒把這個型別寫出來過——變數的型別都靠 Rust 自己推導,函數參數也只寫了切片 &[i32]。如果哪天你需要親手寫出它(比如當函數的參數或回傳值型別),該怎麼寫?還有上面表格裡 &[T] 的那個 T,到底可以是什麼?這兩個問題的答案是同一個機制——下一章會花大量篇幅討論。
重點整理
Vec和String的所有權行為完全對稱:資料都主要在 heap 上、都會 move、都可以clone。String↔&str就像Vec↔&[T](擁有 ↔ 借用)。&Vec會自動轉成&[T](跟&String自動轉&str一樣)。- 函數參數偏好用切片
&[T]而不是&Vec。 for x in v:move,消耗整個Vec。for x in &v:borrow,只是借用,Vec還在;這個例子裡的x是參考,型別是&i32。- 大部分情況用
for x in &v,除非你確定不再需要這個Vec。 - 我們始終沒把「裝
i32的Vec」的型別親手寫出來——怎麼寫,以及&[T]的T是什麼,下一章揭曉。
恭喜你完成了第 4 章!🎉 這一章你學會了 Rust 最核心的概念——所有權、move、clone、Copy、borrowing,還有 String 和 Vec 這兩個最常用的非 Copy 型別。這些概念是 Rust 和其他語言最大的不同,也是 Rust 能在不需要犧牲效能的情況下保證記憶體安全的關鍵。下一章我們將進入泛型、trait bound 和生命週期——讓你的程式碼能處理任意型別,同時保持型別安全!