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.

rust
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:

text
Harga asli: Rp50000
Harga salinan: Rp50000

Tidak ada error dan kedua baris tercetak — inilah perilaku Copy. Mari kita bedah alasannya:

  1. Baris 2 (let original_price = 50000;): nilai i32 senilai 50000 disimpan utuh di stack. Tidak ada pointer, tidak ada data terpisah di heap.
  2. Baris 3 (let price_copy = original_price;): karena i32 mengimplementasikan Copy, Rust menyalin 4 byte nilai tersebut ke slot stack milik price_copy. Variabel original_price sama sekali tidak divalidasi.
  3. 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.

rust
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:

text
Daftar asli: Budi, Siti, Andi
Daftar salinan: Budi, Siti, Andi

Kali ini kedua variabel sah dipakai meskipun original_list bertipe String. Mari kita bedah perbedaannya:

  1. Baris 2 (let original_list = ...): original_list menjadi pemilik teks di heap, seperti biasa.
  2. 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 oleh list_copy. Karena heap yang dipakai berbeda, penugasan ini bukan move — original_list tetap pemilik sah atas datanya.
  3. 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.

rust
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:

text
Nomor punggung: 7
Nama pemain: Budi
Nomor dari tuple: 7

Perhatikan bahwa baris terakhir memakai player_data.0 — tuple yang salah satu field-nya sudah berpindah. Mari kita bedah mengapa kode ini sah:

  1. Baris 2 (let player_data = ...): tuple berisi i32 bernilai 7 dan String bernilai "Budi". Tuple ini tidak Copy karena anggota keduanya bukan Copy.
  2. Baris 4 (let jersey_number = player_data.0;): field .0 bertipe i32 sehingga tersalin penuh. Field .0 di dalam player_data tetap sah.
  3. Baris 5 (let player_name = player_data.1;): field .1 bertipe String sehingga terjadi partial move — kepemilikan teks "Budi" berpindah ke player_name. Sejak titik ini, player_data.1 tidak boleh dipakai lagi.
  4. Baris 9 (println!("Nomor dari tuple: {}", player_data.0);): akses ke .0 tetap sah karena field tersebut tersalin, bukan berpindah. Sebaliknya, akses ke .1 akan 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:

rust
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:

text
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 nilaiTrait yang dimilikiPerilaku penugasanStatus variabel lama
i32, f64, bool, charCopyDisalin otomatisTetap sah
Tuple yang seluruh anggotanya CopyCopyDisalin otomatisTetap sah
String dan tipe berbasis heapClone (tanpa Copy)Berpindah (move)Tidak valid
Tuple campuranClone (tanpa Copy)Pindah per fieldSebagian 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.