Copy
本集目標
理解 Copy trait——為什麼整數、浮點數、布林值、字元在賦值時不會 move。
概念說明
上一集的問題
上一集我們發現,struct 的值在賦值或傳入函數時會被 move,但整數不會:
fn main() {
let a = 42;
let b = a;
println!("{}", a); // 完全沒問題!
}
為什麼?答案就是 Copy trait。
什麼是 Copy?
Copy 是一個特殊的 trait。如果一個型別實作了 Copy,那麼在賦值或傳入函數時,Rust 會自動複製一份,而不是 move。
Copy 表示 copy 這個值是單純、機械化的操作,所以 Rust 可以在賦值或傳入函數時自動完成,不用特別寫 .clone()。
哪些型別自動有 Copy?
以下這些型別天生就有 Copy:
- 整數:
i8,i16,i32,i64,i128,u8,u16,u32,u64,u128,isize,usize。 - 浮點數:
f32,f64。 - 布林值:
bool。 - 字元:
char。 - ……還有其他更多型別。
另外,tuple 和陣列如果裡面每個元素都是 Copy 的,那它們整體也是 Copy 的:
fn main() {
let t = (1, true, 'a'); // (i32, bool, char) → 全部 Copy → tuple 也是 Copy
let t2 = t;
println!("{:?}", t); // OK!
let arr = [1, 2, 3]; // [i32; 3] → i32 是 Copy → 陣列也是 Copy
let arr2 = arr;
println!("{:?}", arr); // OK!
}
這就是為什麼你在前面幾章寫的程式碼裡,整數、tuple、陣列可以隨便賦值給多個變數、傳進多個函數,完全不會有問題。
除了 Copy 之外,當 tuple 的每個型別都有實作 Clone 時,tuple 也會自動實作 Clone。事實上 tuple 對一些其他 trait 也有同樣的行為——對這些 trait 而言,只要所有元素都有實作,tuple 整體就會有。這點以後不再贅述。
自己的型別也可以加 Copy
如果你的型別裡面所有值都是 Copy 的型別,那你的型別也可以加上 #[derive(Copy, Clone)]:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {}
注意:Copy 要求 Clone。使用 derive 時,通常會寫成 #[derive(Copy, Clone)];如果型別沒有實作 Clone,只寫 #[derive(Copy)] 就會編譯失敗。
為什麼?因為 Rust 規定:任何可以 copy 的東西,也必須可以 clone。Copy 是「自動複製」,Clone 是「手動呼叫 .clone()」。如果一個東西連手動 clone 都做不到,那自動複製當然更不該發生。所以 Copy 要求你先有 Clone。
加上之後,Point 的行為就跟整數一樣了——賦值不會 move:
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // 自動複製,p1 還在!
println!("{:?}", p1); // OK
}
copy 和 clone 的差別
| copy | clone | |
|---|---|---|
| 觸發方式 | 自動(賦值、傳入函數) | 手動(.clone()) |
| 能否自訂行為 | 不能 | 可以,透過 .clone() 的實作決定 |
| 實作前提 | 內容都必須是 Copy | 沒有限制 |
簡單來說:copy 是自動的複製;clone 要手動呼叫,做什麼由型別自己決定。
範例程式碼
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn print_point(p: Point) {
println!("函數收到:({}, {})", p.x, p.y);
}
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
// 整數自動 Copy
let a = 42;
let b = a;
println!("a = {}, b = {}", a, b); // 兩個都能用
// bool 也是 Copy
let flag = true;
let flag2 = flag;
println!("flag = {}, flag2 = {}", flag, flag2);
// 整數傳進函數不會 move
let x = 10;
let result = double(x);
println!("x = {}, result = {}", x, result);
// 自訂的 struct 加上 Copy 後也不會 move
let p1 = Point { x: 3, y: 7 };
let p2 = p1; // 自動複製
print_point(p1); // p1 還能用
println!("p1 = {:?}", p1); // 還是能用!
println!("p2 = {:?}", p2);
}
不要隨便幫自己的型別加 Copy
看完這一集,你可能會想:「那我以後每個 struct 都加 #[derive(Copy, Clone)] 不就好了?」
請不要這樣做。 原因是:一旦加了 Copy,使用你這個型別的程式碼就會依賴「賦值時自動複製」的行為。如果有一天你需要修改這個 struct,加了一個不是 Copy 的欄位,你就必須拿掉 Copy。
問題來了:拿掉 Copy 之後,原本寫 let p2 = p1; 的地方全部會從「自動複製」變成「move」,p1 就不能再用了。所有用到這個型別的程式碼都可能因此壞掉,而且壞的地方可能很多、很分散。
所以好的習慣是:只有當你確定這個型別永遠不會再加入非 Copy 欄位時,才加 Copy。 像 Point { x: i32, y: i32 } 這種就很適合。如果不確定,只加 Clone 就好——需要的時候手動寫 .clone(),未來要改也不會影響其他程式碼。
重點整理
Copy是一個trait,讓型別在賦值和傳入函數時自動複製,而不是 move。i32、f64、bool、char等基本型別天生就有Copy。- tuple 和陣列如果所有元素都是
Copy,整體也是Copy。 - tuple 對一些
trait(Copy、Clone等)有同樣的行為:對這些trait而言,所有元素都有實作 → tuple 就有實作。 - 自訂
struct可以加#[derive(Copy, Clone)],但所有欄位都必須是Copy的型別。 Copy要求Clone;使用 derive 時,通常會把兩者一起寫上。Copy= 自動複製,Clone= 手動呼叫.clone()- 不要隨便加
Copy——未來拿掉會讓所有依賴自動複製的程式碼壞掉。不確定就只加Clone。