Topik ini membahas tiga cara Rust menanggapi penugasan dan penyalinan nilai, yaitu penyalinan otomatis pada tipe Copy, salinan dalam dengan metode clone, dan salinan dangkal pada tipe yang berbagi data. Pembahasan mencakup daftar tipe yang menyalin secara otomatis, alasan String dikecualikan, serta biaya yang menyertai setiap pilihan. Setelah membaca topik ini, keputusan antara membiarkan move terjadi, memakai clone, atau mengandalkan tipe Copy dapat diambil dengan menimbang kepemilikan data dan biaya penyalinannya.
Tipe Copy: salinan otomatis di stack#
Copy adalah trait yang menandai tipe yang penyalinannya cukup dilakukan bit demi bit di stack, tanpa menyentuh alokasi heap mana pun. Ketika sebuah nilai bertipe Copy ditugaskan ke variabel lain atau dikirim ke fungsi, kompilator menduplikasi nilainya dan variabel lama tetap valid. Penyalinan seperti ini murah karena ukurannya tetap dan sudah diketahui saat kompilasi, sehingga tidak ada alasan untuk memindahkan kepemilikan.
Tipe yang mengimplementasikan Copy meliputi semua tipe bilangan bulat dan pecahan, bool, char, serta referensi baca &T untuk tipe apa pun. Tuple juga Copy selama seluruh anggotanya Copy. Referensi tulis &mut T sengaja tidak Copy, karena penggandaannya akan menghasilkan dua akses tulis pada satu data dan melanggar aturan pinjaman. Program berikut memperlihatkan penugasan berulang tanpa satu pun variabel menjadi tidak valid.
fn main() {
let x = 5;
let y = x; // i32 bertipe Copy: nilai disalin penuh, x tetap valid
println!("x = {}, y = {}", x, y);
let asal = (10, 4.5, true);
let salinan = asal; // tuple yang seluruh anggotanya Copy ikut tersalin
println!("asal = {:?}", asal);
println!("salinan = {:?}", salinan);
let rujukan = &x;
let rujukan_lain = rujukan; // &T juga mengimplementasikan Copy
println!("rujukan = {}", rujukan);
println!("rujukan_lain = {}", rujukan_lain);
}x = 5, y = 5
asal = (10, 4.5, true)
salinan = (10, 4.5, true)
rujukan = 5
rujukan_lain = 5Keluaran tersebut menunjukkan bahwa x tetap tercetak setelah nilainya ditugaskan ke y, dan asal tetap tercetak setelah ditugaskan ke salinan. Kedua pasang variabel tersebut memegang nilai yang saling merdeka, sehingga mengubah salah satunya tidak mengubah pasangannya. Perilaku ini berbeda dari String, yang penugasannya selalu berupa move seperti dibahas pada topik Aturan Ownership.
String tidak mengimplementasikan Copy#
String tidak dapat mengimplementasikan Copy karena strukturnya memuat penunjuk ke heap. Penyalinan bit demi bit atas struktur kendalinya akan menghasilkan dua penunjuk pada satu penyangga, sehingga dua pemilik membebaskan memori yang sama saat lingkup masing-masing berakhir. Penyalinan yang benar harus menggandakan isi penyangganya, dan biaya sebesar itu tidak boleh disembunyikan di balik penugasan biasa. Karena itu kompilator memilih memindahkan kepemilikan dan menandai variabel lama sebagai tidak valid.
Aturan yang sama menjalar ke tipe majemuk. Sebuah tuple hanya Copy apabila seluruh anggotanya Copy, sehingga satu anggota bertipe String saja sudah membuat seluruh tuple kehilangan status Copy. Contoh berikut memperlihatkan penugasan yang ditolak kompilator karena satu anggota tuple bertipe String.
// tidak-dijalankan
fn main() {
let berkas = (String::from("log-harian"), 1024);
let salinan = berkas; // tuple ini tidak Copy karena berisi String
println!("ukuran: {}", salinan.1);
println!("nama: {}", berkas.0); // ditolak: berkas sudah dipindahkan
}Pada contoh tersebut, anggota kedua bertipe i32 sebenarnya Copy, tetapi tuple keseluruhannya mengikuti anggota yang paling ketat. Setelah let salinan = berkas;, kepemilikan seluruh tuple berpindah. Pemakaian berkas.0 pada baris terakhir memicu E0382, yakni error yang sama dengan pemakaian String yang sudah dipindahkan, dengan keterangan bahwa tipe tuple tersebut tidak mengimplementasikan Copy.
clone: salinan dalam yang eksplisit#
Metode clone() membuat salinan penuh atas sebuah nilai beserta data yang ditunjuknya di heap, sehingga hasilnya disebut salinan dalam. Pada String, metode ini mengalokasikan penyangga baru, menyalin seluruh isi teks ke penyangga tersebut, lalu membangun struktur kendali yang menunjuk penyangga baru itu. Kedua nilai hasilnya saling merdeka: mengubah salah satu tidak menyentuh yang lain.
Biaya clone() sebanding dengan ukuran data yang disalin, sehingga Rust mensyaratkan pemakaiannya ditulis secara eksplisit agar setiap salinan mahal terlihat di kode. Sifat eksplisit ini juga memudahkan penelusuran biaya, karena pencarian kata clone pada berkas sumber langsung menampilkan seluruh titik penyalinan mahal. Untuk tipe bentukan sendiri, implementasi Clone dapat dihasilkan otomatis dengan anotasi #[derive(Clone)] selama seluruh anggotanya juga menerapkan Clone.
fn main() {
let s1 = String::from("arsip");
let mut s2 = s1.clone(); // data di heap ikut disalin
println!("s1 = {}, s2 = {}", s1, s2);
s2.push_str("-cadangan");
println!("s2 setelah push = {}", s2);
println!("s1 tetap utuh = {}", s1);
}s1 = arsip, s2 = arsip
s2 setelah push = arsip-cadangan
s1 tetap utuh = arsipProgram tersebut menambahkan teks pada s2 setelah penyalinan, dan keluarannya menunjukkan bahwa s1 tidak ikut berubah. Diagram berikut memperlihatkan keadaan memori setelah clone(): kedua variabel menunjuk penyangga yang berbeda dan berisi salinan data yang sama.
Clone dangkal: berbagi tanpa menyalin data#
Tidak semua implementasi clone() menyalin data. Tipe Rc dari pustaka standar menyimpan nilai di heap bersama sebuah penghitung pemilik, dan Rc::clone hanya menaikkan penghitung tersebut tanpa menyentuh datanya. Salinan semacam ini disebut salinan dangkal, karena hanya pegangannya yang digandakan, bukan isinya. Ketika penghitung mencapai nol, pemilik terakhir menjatuhkan nilainya dan memori heap pun dibebaskan.
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("log-bersama"));
let b = Rc::clone(&a); // salinan dangkal: hanya penghitung yang naik
println!("jumlah pemilik : {}", Rc::strong_count(&a));
println!("a = {}, b = {}", a, b);
println!("alamat sama : {}", Rc::ptr_eq(&a, &b));
}jumlah pemilik : 2
a = log-bersama, b = log-bersama
alamat sama : trueKeluaran tersebut memuat tiga bukti sekaligus. Baris pertama menunjukkan penghitung pemilik bernilai dua setelah Rc::clone, baris kedua menunjukkan kedua pegangan membaca data yang sama, dan baris alamat sama dengan nilai true menegaskan bahwa a dan b menunjuk alokasi heap yang sama. Bandingkan dengan clone() pada String yang menghasilkan dua penyangga berbeda, sehingga pemeriksaan kesamaan alamat akan menghasilkan false. Perbedaan kedua jenis clone tersebut dirangkum pada tabel berikut.
| Aspek | clone pada String | Rc::clone |
|---|---|---|
| Data di heap | disalin penuh ke penyangga baru | tidak disalin, penghitung pemilik naik satu |
| Jumlah pemilik data | satu pemilik per salinan | banyak pemilik untuk satu data |
| Biaya | sebanding dengan ukuran data | tetap kecil berapa pun ukuran data |
Memilih antara Copy, clone, dan move#
Pilihan ketiganya mengikuti satu pertanyaan, yaitu berapa pihak yang perlu mengakses data dan dalam mode apa. Untuk data kecil berukuran tetap seperti angka dan tanda, biarkan tipe Copy bekerja karena penyalinannya lebih murah daripada pemindahan kepemilikannya. Untuk data heap yang memang perlu dimiliki dua pihak secara merdeka, gunakan clone() dan terima biayanya sebagai bagian dari desain. Untuk data heap yang cukup dibaca bersama, pindahkan kepemilikan atau pinjam lewat referensi, dan jadikan Rc pilihan hanya ketika umur data memang harus melebihi satu lingkup pemilik.
Kerangka yang sama berlaku pada perancangan antarmuka fungsi. Parameter bertipe &str atau &[T] memaksa mode pinjam dan menyerahkan keputusan salin atau pindah kepada pemanggil, sedangkan parameter bertipe String memaksa mode milik. Konsekuensinya, penyalinan mahal menjadi pilihan yang terlihat di titik pemanggilan, bukan keputusan yang tersembunyi di dalam fungsi. Aturan praktisnya adalah memulai dari pinjaman baca, menaikkan ke pinjaman tulis apabila memang perlu mengubah, dan menyerahkan kepemilikan hanya ketika fungsi bermaksud menyimpan atau menghabiskan datanya.
Kesalahan umum#
Kesalahan pertama adalah menambahkan clone() begitu error kepemilikan muncul, tanpa menimbang apakah salinan itu diperlukan. Salinan dalam yang tidak perlu menggandakan alokasi heap dan kerap menutupi masalah desain yang sesungguhnya, misalnya fungsi yang seharusnya meminjam dengan &str alih-alih menerima String. Perbaikannya dimulai dari membaca pesan errornya: apabila kompilator mengusulkan pemakaian clone, pertimbangkan dahulu apakah mengubah tanda tangan fungsi agar meminjam sudah memadai. Apabila peninjauan menemukan clone() di dalam perulangan, periksa apakah salinan itu dapat dikeluarkan dari perulangan atau diganti pinjaman baca.
Kesalahan kedua adalah menganggap semua tipe sederhana otomatis Copy. Struct yang memuat satu saja medan bertipe String atau Vec tidak Copy, dan tuple pun mengikuti anggotanya yang paling ketat. Periksa dokumentasi tipe atau biarkan kompilator menyatakan hal tersebut lewat error E0382 seperti pada contoh tuple di atas. Aturan praktisnya mudah diingat: nilai yang dapat memicu alokasi atau pembebasan heap tidak pernah Copy, karena penyalinan diam-diam atas nilai seperti itu akan menyembunyikan biaya besar di balik penugasan biasa.
Ringkasan#
- Tipe
Copydisalin bit demi bit di stack secara otomatis, mencakup bilangan,bool,char,&T, dan tuple yang seluruh anggotanyaCopy. StringtidakCopykarena penyalinan bit demi bit akan menghasilkan dua pemilik untuk satu penyangga heap.clone()membuat salinan dalam yang eksplisit dengan biaya sebanding ukuran data, sedangkanRc::clonemembuat salinan dangkal dengan hanya menaikkan penghitung pemilik.- Keputusan antara membiarkan move, menyalin, atau berbagi ditentukan oleh jumlah pihak pengakses dan mode aksesnya.
Referensi#
- What is Ownership? — The Rust Programming Language — Bagian yang menjelaskan tipe Copy dan clone sebagai jalan tengah atas semantik move.
- Trait Copy — Rust standard library — Dokumentasi trait Copy beserta daftar tipe yang mengimplementasikannya.
- Struct Rc — Rust standard library — Dokumentasi tipe Rc yang menjadi contoh clone dangkal pada topik ini.