Topik ini membahas tiga aturan ownership yang menjadi fondasi pengelolaan memori di Rust beserta akibat praktisnya pada kode sehari-hari. Pembahasan mencakup anatomi String di memori, semantik move yang memindahkan kepemilikan pada penugasan, pemanggilan drop saat lingkup berakhir, dan perpindahan kepemilikan ketika nilai dikirim ke atau dikembalikan dari sebuah fungsi. Setelah membaca topik ini, alasan sebuah variabel menjadi tidak valid setelah ditugaskan ke variabel lain dapat dijelaskan tanpa menebak, dan pesan error yang muncul akibat perpindahan tersebut dapat dibaca sampai ke akar masalahnya.
Tiga aturan ownership#
Seluruh perilaku kepemilikan di Rust berdiri di atas tiga aturan yang ringkas dan berlaku untuk semua nilai, baik yang hidup di stack maupun di heap.
- Setiap nilai di Rust memiliki variabel yang disebut pemiliknya (owner).
- Sebuah nilai hanya memiliki satu pemilik pada satu waktu.
- Ketika pemilik keluar dari lingkup, nilai dijatuhkan dengan memanggil
drop.
Aturan ketiga terikat pada konsep lingkup, yaitu wilayah program di antara sepasang kurung kurawal. Sebuah variabel mulai hidup pada titik deklarasinya dan berhenti hidup saat kurung kurawal penutup lingkupnya tercapai. Nilai bertipe sederhana seperti u32 cukup dilepaskan dari stack, sedangkan nilai seperti String juga membebaskan alokasi heap yang ditunjuknya. Setelah pelepasan tersebut selesai, nama variabelnya tidak dapat dipakai lagi di titik mana pun pada program.
Kombinasi tiga aturan tersebut menjawab pertanyaan yang pada bahasa lain ditangani runtime: siapa yang berhak membebaskan sebuah memori, dan kapan pembebasan itu terjadi. Karena jawabannya selalu satu nama variabel dan satu titik di dalam kode, kompilator dapat memasang pemeriksaannya pada saat kompilasi tanpa menyisakan pekerjaan apa pun pada saat program berjalan.
Anatomi String di memori#
String terdiri atas dua bagian yang hidup terpisah, dan pemahaman anatomi tersebut diperlukan sebelum membahas move. Bagian pertama adalah struktur kendali berukuran 24 byte di stack yang memuat tiga medan: penunjuk (ptr) yang menyimpan alamat awal penyangga di heap, panjang (len) yang menyimpan banyaknya byte yang sedang dipakai, dan kapasitas (capacity) yang menyimpan banyaknya byte yang sudah dialokasikan. Bagian kedua adalah penyangga di heap yang menampung isi teksnya. Nilai len selalu kurang dari atau sama dengan capacity, dan selisih keduanya adalah ruang cadangan yang belum terpakai.
Ketika pemilik sebuah String keluar dari lingkup, Rust memanggil drop atas nilai tersebut. Pemanggilan itu membebaskan penyangga di heap terlebih dahulu, lalu melepas struktur kendalinya dari stack. Karena penyangga hanya boleh dimiliki satu pihak pada satu waktu, pembebasan itu selalu terjadi tepat satu kali. Sifat inilah yang membuat sejumlah pilihan desain pada bagian-bagian berikutnya masuk akal.
Semantik move: kepemilikan berpindah#
Penugasan let s2 = s1; pada nilai bertipe String tidak menyalin data di heap, melainkan memindahkan kepemilikan, dan proses penyerahan seperti ini disebut move. Ada dua alasan untuk pilihan tersebut. Menyalin seluruh isi heap menimbulkan biaya besar untuk teks yang panjang, sedangkan menyalin struktur kendalinya saja menghasilkan dua penunjuk pada satu penyangga yang sama, sehingga drop berjalan dua kali pada memori yang sama. Untuk menjaga keamanan memori tanpa menanggung biaya penyalinan, Rust menjadikan s1 tidak valid setelah penugasan dan mengakui s2 sebagai satu-satunya pemilik baru. Pada tingkat mesin, yang berpindah hanyalah isi struktur kendali di stack, yaitu tiga bilangan sepanjang 64 bit, sehingga penyerahan ini selesai hampir seketika berapa pun panjang teks yang dibawanya.
fn main() {
let s1 = String::from("baterai");
// kepemilikan berpindah dari s1 ke s2: s1 tidak valid lagi
let s2 = s1;
println!("s2 menyimpan: {}", s2);
println!("Panjang s2 : {}", s2.len());
} // s2 keluar dari lingkup, memori "baterai" dikembalikan
s2 menyimpan: baterai
Panjang s2 : 7Keluaran program memperlihatkan bahwa s2 berfungsi sepenuhnya sebagai pemilik baru: isinya tercetak, panjangnya dapat dihitung, dan memori heapnya dikembalikan saat main selesai. Program sengaja tidak memakai s1 setelah baris penugasan, karena pemakaian seperti itu ditolak oleh kompilator. Diagram berikut merangkum keadaan memori setelah let s2 = s1; dijalankan.
Nilai yang sudah dipindahkan tidak dapat dipakai#
Kompilator menolak pemakaian variabel yang kepemilikannya sudah berpindah, dan penolakan tersebut dapat diperiksa langsung dengan rustc. Program berikut sengaja memakai nama setelah nilainya dikirim ke fungsi proses, sehingga kompilasi gagal dengan kode error E0382.
export PATH="$HOME/.cargo/bin:$PATH"
# contoh kode yang ditolak kompilator: nilai dipakai setelah dipindahkan
printf 'fn proses(teks: String) -> usize {\n teks.len()\n}\n\nfn main() {\n let nama = String::from("sensor");\n proses(nama);\n println!("Panjang teks: {}", nama.len());\n}\n' > salah.rs
rustc --edition=2021 salah.rs 2>&1 | sed -n '1,9p'error[E0382]: borrow of moved value: `nama`
--> salah.rs:8:34
|
6 | let nama = String::from("sensor");
| ---- move occurs because `nama` has type `String`, which does not implement the `Copy` trait
7 | proses(nama);
| ---- value moved here
8 | println!("Panjang teks: {}", nama.len());
| ^^^^ value borrowed here after moveKeluaran tersebut memuat tiga baris kunci yang saling melengkapi. Baris move occurs because 'nama' has type 'String', which does not implement the 'Copy' trait menyatakan alasan perpindahan, yaitu String bukan tipe yang disalin otomatis. Baris value moved here menunjuk baris tujuh sebagai titik kepemilikan berpindah ke parameter teks, dan baris value borrowed here after move menunjuk baris kedelapan sebagai titik pelanggaran. Tanpa pemeriksaan ini, penyangga heap milik nama akan dibebaskan dua kali, yakni sekali oleh drop untuk parameter di akhir fungsi proses dan sekali lagi oleh drop untuk nama di akhir main. Pembebasan ganda seperti itu merusak pengelola memori dan termasuk kelas kesalahan paling sulit dilacak pada bahasa tanpa pemeriksaan kepemilikan.
Penolakan yang sama berlaku pada penugasan biasa di dalam satu fungsi. Mengganti pemanggilan proses dengan let nama2 = nama; lalu mencetak nama menghasilkan E0382 dengan struktur pesan yang serupa, karena yang berubah hanyalah letak titik perpindahan, bukan maknanya. Keluaran pada blok di atas dipotong pada baris kesembilan, sedangkan pesan aslinya masih berlanjut dengan usulan perbaikan, antara lain mengubah parameter fungsi proses agar meminjam alih-alih memiliki. Penjelasan umum untuk kode ini juga tersedia melalui perintah rustc --explain E0382, yang memuat contoh terformat beserta ringkasannya.
Ownership pada pemanggilan fungsi#
Mengirim nilai ke fungsi melalui parameter juga merupakan move, karena parameter adalah variabel milik fungsi yang dipanggil. Ketika fungsi selesai, parameter berikut data yang dibawanya dijatuhkan, kecuali fungsi mengembalikannya ke pemanggil. Nilai kembalian juga berpindah kepemilikan, kali ini dari fungsi ke variabel penerima di pemanggil. Pola “menyerahkan lalu menerima kembali” seperti ini sah, tetapi menjadi bertele-tele apabila fungsi hanya perlu membaca datanya.
fn bubuhkan_awalan(teks: String) -> String {
format!("catatan: {}", teks)
}
fn main() {
let baris = String::from("suhu 30 derajat");
// kepemilikan baris masuk ke fungsi lalu kembali sebagai nilai baru
let baris = bubuhkan_awalan(baris);
println!("{}", baris);
}catatan: suhu 30 derajatPada program tersebut, baris yang pertama kehilangan kepemilikan saat dikirim ke bubuhkan_awalan, kemudian variabel baris yang baru dideklarasikan menampung nilai kembalian dari fungsi. Isi lama "suhu 30 derajat" dijatuhkan di dalam fungsi, sedangkan isi baru "catatan: suhu 30 derajat" dibawa pulang oleh nilai kembalian. Pernyataan let yang kedua melakukan shadowing, sehingga nama baris tetap terpakai tanpa perlu nama sementara tambahan.
Pola ini menjadi bertambah repot ketika satu fungsi perlu mengubah dan mengembalikan beberapa nilai sekaligus, karena seluruh nilai itu harus dibungkus tuple atau struct agar dapat dibawa pulang bersama. Penulisan tambahan seperti ini bukan sekadar persoalan gaya, melainkan tanda bahwa kepemilikan berpindah terlalu jauh untuk pekerjaan yang sebenarnya hanya membaca dan menulis isi data. Mekanisme yang lebih ringan untuk situasi baca dan tulis tanpa perpindahan kepemilikan dibahas pada topik Referensi dan Borrowing.
Kesalahan umum#
Kesalahan pertama adalah membawa kebiasaan dari bahasa dengan garbage collection, yaitu menganggap let s2 = s1; membuat dua nama untuk satu objek yang sama. Pada Rust, penugasan seperti itu memindahkan kepemilikan dan menonaktifkan nama lama, sehingga pemakaian s1 setelahnya ditolak. Perbaikannya bergantung pada tujuannya: pakai .clone() apabila dua nilai merdeka memang diperlukan, atau pinjam lewat referensi apabila cukup membaca. Membaca baris value moved here pada pesan error terlebih dahulu menghindarkan perbaikan yang menebak, karena baris itu menyatakan dengan tepat di mana kepemilikan berpindah.
Kesalahan kedua adalah menganggap fungsi yang menerima String mengembalikan kepemilikan secara otomatis setelah selesai. Kepemilikan berakhir di parameter dan memori dibebaskan saat fungsi berakhir, kecuali nilai dikembalikan secara eksplisit lewat nilai kembalian. Konsekuensinya, fungsi yang menghabiskan parameternya memaksa pemanggil menyiapkan nilai baru, dan pemanggilan berantai dari fungsi seperti itu menambah satu penyerahan dan satu penerimaan di setiap lapisan. Bila pola itu tidak diinginkan, ubah tanda tangan fungsinya agar meminjam data dengan &String atau &str.
Ringkasan#
- Setiap nilai memiliki satu pemilik, kepemilikan berpindah lewat move, dan
dropdipanggil ketika pemilik keluar dari lingkup. Stringterdiri atas struktur kendali 24 byte di stack dan penyangga di heap yang ditunjuk medanptr.- Penugasan
let s2 = s1;memindahkan kepemilikan tanpa menyalin heap, sehinggas1tidak valid lagi dan pemakaiannya memicu error E0382. - Parameter fungsi menerima kepemilikan lewat move, dan kepemilikan hanya kembali ke pemanggil melalui nilai kembalian.
Referensi#
- What is Ownership? — The Rust Programming Language — Bab resmi yang memuat tiga aturan ownership, anatomi String, serta semantik move dan drop.
- Scope and move — Rust by Example — Contoh ringkas perpindahan kepemilikan pada penugasan dan pemanggilan fungsi.
- Move semantics — Rust Reference — Definisi formal nilai yang dipindahkan dan disalin pada ekspresi Rust.