Move vs Clone

Tipe Copy, clone dalam, dan clone dangkal.

Daftar isi

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.

rust tipe-copy.rs
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);
}
text Keluaran
x = 5, y = 5
asal = (10, 4.5, true)
salinan = (10, 4.5, true)
rujukan = 5
rujukan_lain = 5

Keluaran 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.

rust Contoh: tuple yang berisi String tidak Copy
// 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.

rust clone-dalam.rs
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);
}
text Keluaran
s1 = arsip, s2 = arsip
s2 setelah push  = arsip-cadangan
s1 tetap utuh    = arsip

Program 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.

s2 = s1.clone()data di heap disalin penuh, dua pemilik merdekas1 (String)s2 (String)"arsip""arsip"(salinan di heap baru)stack
clone pada String menghasilkan dua penyangga heap terpisah dengan isi 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.

rust rc-dangkal.rs
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));
}
text Keluaran
jumlah pemilik : 2
a = log-bersama, b = log-bersama
alamat sama    : true

Keluaran 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.

Aspekclone pada StringRc::clone
Data di heapdisalin penuh ke penyangga barutidak disalin, penghitung pemilik naik satu
Jumlah pemilik datasatu pemilik per salinanbanyak pemilik untuk satu data
Biayasebanding dengan ukuran datatetap 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 Copy disalin bit demi bit di stack secara otomatis, mencakup bilangan, bool, char, &T, dan tuple yang seluruh anggotanya Copy.
  • String tidak Copy karena penyalinan bit demi bit akan menghasilkan dua pemilik untuk satu penyangga heap.
  • clone() membuat salinan dalam yang eksplisit dengan biaya sebanding ukuran data, sedangkan Rc::clone membuat salinan dangkal dengan hanya menaikkan penghitung pemilik.
  • Keputusan antara membiarkan move, menyalin, atau berbagi ditentukan oleh jumlah pihak pengakses dan mode aksesnya.

Referensi#