Setelah memahami move semantics, kita tahu bahwa let b = a; membuat variabel a tidak valid ketika a bertipe String. Namun pembaca yang jeli akan segera menemukan pengecualian: kode seperti let x = 5; let y = x; berjalan mulus dan kedua variabel tetap bisa dipakai. Ternyata tidak semua tipe ikut aturan move. Perbedaan ini diatur oleh dua trait bernama Copy dan Clone, dan pembedaan keduanya adalah kunci menulis kode Rust yang efisien serta memahami pesan error E0382 yang muncul berulang kali. Pada Chapter ini kita akan memetakan tipe mana yang disalin otomatis, tipe mana yang harus di-clone secara eksplisit, dan bagaimana keduanya berinteraksi pada tuple.
Tipe Copy: Penyalinan Murah yang Otomatis #
Tipe-tipe yang seluruh datanya hidup di stack dan ukurannya tetap mendapat perlakuan istimewa: penugasan menyalin nilainya secara penuh, dan variabel lama tetap sah. Perilaku ini diatur oleh trait Copy. Kelompok tipe Copy mencakup seluruh bilangan bulat (i32, u64, dan sejenisnya), bilangan desimal (f64), bool, char, serta tuple yang seluruh anggotanya juga Copy.
fn main() {
let original_price = 50000;
let price_copy = original_price;
println!("Harga asli: Rp{}", original_price);
println!("Harga salinan: Rp{}", price_copy);
}Output yang Dihasilkan:
Harga asli: Rp50000
Harga salinan: Rp50000Tidak ada error dan kedua baris tercetak — inilah perilaku Copy. Mari kita bedah alasannya:
- Baris 2 (
let original_price = 50000;): nilaii32senilai50000disimpan utuh di stack. Tidak ada pointer, tidak ada data terpisah di heap. - Baris 3 (
let price_copy = original_price;): karenai32mengimplementasikanCopy, Rust menyalin 4 byte nilai tersebut ke slot stack milikprice_copy. Variabeloriginal_pricesama sekali tidak divalidasi. - Baris 5–6 (
println! ...): kedua variabel tetap sah dan sama-sama dapat dicetak, terbukti dari output yang muncul dua kali.
Penyalinan semacam ini aman karena kedua variabel kini memegang nilai yang benar-benar terpisah. Ketika masing-masing keluar scope, masing-masing hanya melepas slot stack-nya sendiri — tidak ada heap yang bisa di-drop dua kali. Karena itulah Copy diperbolehkan: biayanya cuma beberapa byte di stack dan tidak pernah membuka celah double free.
Tipe Non-Copy: Data Heap Wajib Dipindahkan #
String tidak pernah bisa menjadi Copy. Sebabnya sudah kita bahas di Chapter Move Semantics: data pengelola String di stack mengandung pointer ke heap, dan menyalin data pengelola saja akan menghasilkan dua pemilik atas satu wilayah heap — resep bencana double free. Maka Rust menarik garis tegas: tipe yang mengandung data heap, seperti String, tidak mengimplementasikan Copy, sehingga penugasan terhadap tipe tersebut selalu berupa move.
Garis ini juga berlaku komposisinya. Tuple (i32, String) tidak Copy karena salah satu anggotanya bukan Copy — dan kita akan lihat konsekuensi menarik dari kasus campuran ini di bagian akhir chapter.
Clone: Penyalinan Eksplisit untuk Data Heap #
Bagaimana jika kita benar-benar butuh dua salinan String yang sama? Rust menyediakan method clone() untuk itu. Panggilan clone() memesan ruang heap baru, menyalin isi data lama byte demi byte, lalu mengembalikan nilai baru yang sepenuhnya mandiri. Berbeda dari Copy yang bekerja diam-diam di balik operator =, clone() selalu tampak jelas di dalam kode — dan memang direkayasa demikian.
fn main() {
let original_list = String::from("Budi, Siti, Andi");
let list_copy = original_list.clone();
println!("Daftar asli: {}", original_list);
println!("Daftar salinan: {}", list_copy);
}Output yang Dihasilkan:
Daftar asli: Budi, Siti, Andi
Daftar salinan: Budi, Siti, AndiKali ini kedua variabel sah dipakai meskipun original_list bertipe String. Mari kita bedah perbedaannya:
- Baris 2 (
let original_list = ...):original_listmenjadi pemilik teks di heap, seperti biasa. - Baris 3 (
let list_copy = original_list.clone();):clone()memesan wilayah heap baru, menyalin isi"Budi, Siti, Andi"ke sana, lalu hasil salinan itu dimiliki olehlist_copy. Karena heap yang dipakai berbeda, penugasan ini bukan move —original_listtetap pemilik sah atas datanya. - Baris 5–6 (
println! ...): kedua variabel hidup berdampingan tanpa konflik karena dua wilayah heap berdiri terpisah; ketika scope berakhir, masing-masing drop memulangkan wilayahnya sendiri.
Kata kunci dari desain ini adalah eksplisit. Penyalinan data heap bisa mahal — teks sepanjang satu megabyte berarti satu megabyte alokasi dan penyalinan baru. Bahasa lain sering melakukan penyalinan mahal ini diam-diam lewat deep copy otomatis atau reference counting; Rust memaksa kita menandai biaya tersebut dengan .clone() yang terbaca jelas, sehingga keputusan mahal-murahnya ada di tangan kita.
Partial Move pada Tuple Campuran #
Kasus menarik muncul ketika tuple berisi campuran tipe Copy dan non-Copy. Kompilator memperlakukan setiap field tuple secara terpisah: field Copy tersalin, field non-Copy berpindah.
fn main() {
let player_data = (7, String::from("Budi"));
let jersey_number = player_data.0;
let player_name = player_data.1;
println!("Nomor punggung: {}", jersey_number);
println!("Nama pemain: {}", player_name);
println!("Nomor dari tuple: {}", player_data.0);
}Output yang Dihasilkan:
Nomor punggung: 7
Nama pemain: Budi
Nomor dari tuple: 7Perhatikan bahwa baris terakhir memakai player_data.0 — tuple yang salah satu field-nya sudah berpindah. Mari kita bedah mengapa kode ini sah:
- Baris 2 (
let player_data = ...): tuple berisii32bernilai7danStringbernilai"Budi". Tuple ini tidakCopykarena anggota keduanya bukanCopy. - Baris 4 (
let jersey_number = player_data.0;): field.0bertipei32sehingga tersalin penuh. Field.0di dalamplayer_datatetap sah. - Baris 5 (
let player_name = player_data.1;): field.1bertipeStringsehingga terjadi partial move — kepemilikan teks"Budi"berpindah keplayer_name. Sejak titik ini,player_data.1tidak boleh dipakai lagi. - Baris 9 (
println!("Nomor dari tuple: {}", player_data.0);): akses ke.0tetap sah karena field tersebut tersalin, bukan berpindah. Sebaliknya, akses ke.1akan langsung ditolak.
Meski field .0 masih bisa diakses, tuple player_data secara keseluruhan sudah tidak utuh lagi — tuple tersebut tidak bisa dikirim ke fungsi atau dipindahkan ke variabel lain karena salah satu bagiannya sudah hilang. Kondisi partially moved seperti ini sering membingungkan pemula, jadi biasakan memeriksa tipe setiap field ketika kompilator menolak pemakaian tuple.
Untuk membuktikan batasnya, kita coba memakai player_data.1 setelah field tersebut berpindah. Kode berikut sengaja tidak dapat dikompilasi:
fn main() {
let player_data = (7, String::from("Budi"));
let player_name = player_data.1;
println!("Nama pemain: {}", player_name);
println!("Nama dari tuple: {}", player_data.1);
}Pesan error kompilator:
error[E0382]: borrow of moved value: `player_data.1`
--> main.rs:7:37
|
4 | let player_name = player_data.1;
| ------------- value moved here
...
7 | println!("Nama dari tuple: {}", player_data.1);
| ^^^^^^^^^^^^^ value borrowed here after move
|
= note: move occurs because `player_data.1` has type `String`, which does not implement the `Copy` trait
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.Pesan E0382 versi partial move ini menunjuk field secara spesifik: player_data.1 yang berpindah pada baris 4, dan pemakaian ulangnya pada baris 7 yang ditolak. Bandingkan dengan pemakaian .0 pada contoh sebelumnya yang berjalan mulus — kompilator memang menilai setiap field secara terpisah, persis seperti yang kita simpulkan dari tabel rangkuman di bawah.
Kapan Copy, Kapan Clone, Kapan Move #
Kita rangkum keputusan yang diambil Rust pada setiap penugasan let b = a;:
| Tipe nilai | Trait yang dimiliki | Perilaku penugasan | Status variabel lama |
|---|---|---|---|
i32, f64, bool, char | Copy | Disalin otomatis | Tetap sah |
Tuple yang seluruh anggotanya Copy | Copy | Disalin otomatis | Tetap sah |
String dan tipe berbasis heap | Clone (tanpa Copy) | Berpindah (move) | Tidak valid |
| Tuple campuran | Clone (tanpa Copy) | Pindah per field | Sebagian tidak valid |
Ringkasan #
Copy memberi penyalinan otomatis yang murah untuk tipe stack murni, sedangkan Clone menyediakan penyalinan eksplisit berbiaya heap untuk tipe seperti String. Tuple campuran menunjukkan bahwa move dan copy dapat terjadi berdampingan di dalam satu nilai, field demi field. Dengan peta ini, pesan E0382 “does not implement the Copy trait” tidak lagi misterius. Selanjutnya kita membawa pengetahuan ini ke tempat move paling sering terjadi di dunia nyata: pemanggilan fungsi.