Sejak Section Ownership kita menikmati janji paling terkenal Rust: nilai dibersihkan otomatis saat pemiliknya keluar dari scope, tanpa free(), tanpa garbage collector. Namun sampai bab ini, “dibersihkan” selalu kita terima sebagai kotak hitam — siapa sebenarnya yang bekerja di balik layar, dan bisakah kita ikut menulis pekerjaan itu sendiri? Jawabannya adalah trait Drop, mekanisme yang belum pernah kita bahas sepanjang Course ini dan kini tiba waktunya. Drop adalah destructor milik Rust: method yang dijamin dijalankan tepat sebelum nilai benar-benar mati. Trait inilah yang dipakai Box untuk membebaskan heap, dipakai String dan Vec untuk memulangkan memori, dan yang akan kita pakai sendiri untuk sumber daya dunia nyata seperti berkas, koneksi, dan kunci. Untuk pertama kalinya pula, smart pointer akan terlihat bukan hanya sebagai penunjuk memori, melainkan sebagai pemilik tanggung jawab pembersihan.
Masalah: Sumber Daya yang Harus Dibersihkan #
Program nyata tidak hanya mengelola memori. Program membaca berkas log, membuka koneksi database, memegang cache di memori, dan mengunci akses data — dan setiap sumber daya tersebut menuntut ritual penutupan: berkas harus ditutup, koneksi harus diputus, kunci harus dilepas. Di banyak bahasa, ritual ini digantungkan pada pemanggilan manual (close(), dispose()), dan satu return yang terlupa di tengah fungsi cukup untuk membuat sumber daya bocor tanpa terdeteksi.
Rust mengambil jalan yang berbeda dengan pola RAII (Resource Acquisition Is Initialization): sumber daya diikat pada umur sebuah nilai. Selama nilainya hidup, sumber daya dipakai; saat nilai keluar scope, Rust menjalankan method drop milik nilai tersebut sebagai momen resmi penutupan sumber daya. Pemrogram tidak lagi menutup sumber daya secara manual — pemrogram cukup memastikan nilai pemiliknya masih hidup selama sumber daya dibutuhkan, dan kompilator yang memastikan pembersihan selalu terjadi.
Mekanisme Drop di Balik Layar #
Kompilator menyisipkan pemanggilan drop secara otomatis di titik keluar scope — tepat sebelum kurung tutup blok, tepat sebelum return, dan tepat pada setiap jalur match yang mengakhiri fungsi. Method yang dipanggil adalah fn drop(&mut self) dari trait Drop, dan yang mengejutkan bagi pemula: jika satu scope memuat beberapa nilai, Rust menjatuhkannya dalam urutan kebalikan dari deklarasi. Nilai yang dideklarasikan terakhir mati lebih dulu. Alasannya praktis: nilai yang dideklarasikan belakangan sering dibangun di atas nilai yang lebih awal — misalnya writer yang memegang pinjaman dari data — sehingga menyapu yang muda lebih dulu mencegah nilai tua mati ketika masih dipinjam.
Kita juga diam-diam telah bergantung pada Drop sejak lama: String, Vec, dan Box dari pustaka standar semuanya mengimplementasikan Drop untuk membebaskan memori heap. Ketika Chapter Box Dasar menyatakan “petak heap ikut dibebaskan saat Box keluar scope”, pernyataan itu bekerja karena implementasi Drop milik Box.
Drop Pertama: Urutan Pembersihan Terbalik #
Mari kita buktikan urutan terbalik itu dengan struct Resource yang method drop-nya terang-terangan mencetak pesan:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!(" mematikan {}", self.name);
}
}
fn main() {
let _cache = Resource { name: String::from("cache memori") };
let _file = Resource { name: String::from("berkas log") };
let _connection = Resource { name: String::from("koneksi database") };
println!("program utama selesai");
}Output yang Dihasilkan:
program utama selesai
mematikan koneksi database
mematikan berkas log
mematikan cache memoriMari kita bedah kode di atas:
- Baris 5 (
impl Drop for Resource): pendaftaran ritual pembersihan untuk tipe kita sendiri. TraitDropberada di prelude sehingga tidak perluusetambahan. - Baris 6 (
fn drop(&mut self)): signature method wajib — penerima&mut selftanpa nilai kembalian. Rust memberi akses perubahan pada nilai yang sekarat, misalnya untuk menutup berkas atau mencatat status akhir. - Baris 7 (
println!(" mematikan {}", self.name);): pesan cetak inilah “alat ukur” kita — setiap baris pada blok output membuktikan satu eksekusidropyang nyata. - Baris 12-14 (
let _cache = ...dst.): tiga nilai dideklarasikan berurutan: cache, lalu berkas, lalu koneksi. Awalan_pada ketiga nama sengaja dipasang agar kompilator tidak menganggap variabel tersebut terbuang — nilai tetap hidup sampai akhir scope. - Baris 16 (
println!("program utama selesai");): baris ini tercetak paling pertama, membuktikan semuadropmenunggu sampai scopemainbenar-benar usai — bukan saat nilai selesai dipakai. - Urutan blok output: koneksi mati lebih dulu, disusul berkas, terakhir cache — persis kebalikan urutan deklarasi pada Baris 12-14. Inilah bukti langsung urutan pembersihan terbalik.
Drop di Dalam Scope Bersarang #
Nilai tidak harus menunggu akhir fungsi — setiap blok { } adalah scope penuh dengan jadwal drop-nya sendiri:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!(" mematikan {}", self.name);
}
}
fn main() {
let _connection = Resource { name: String::from("koneksi database") };
{
let _file = Resource { name: String::from("berkas log") };
println!("blok dalam selesai");
}
let _cache = Resource { name: String::from("cache memori") };
println!("main selesai");
}Output yang Dihasilkan:
blok dalam selesai
mematikan berkas log
main selesai
mematikan cache memori
mematikan koneksi databaseMari kita bedah kode di atas:
- Baris 12 (
let _connection = ...): koneksi dideklarasikan paling awal sehingga akan mati paling akhir — setelah semua nilai lain dimainberes. - Baris 14-17 (blok
{ }):_filehidup hanya selama blok dalam. Begitu blok ditutup pada Baris 17, Rust menjalankandropuntuk_file— buktinya, pesan “mematikan berkas log” muncul sebelum `main selesai". - Baris 19 (
let _cache = ...): cache dideklarasikan setelah blok dalam selesai, sehingga urutan matinya berada di antara berkas dan koneksi: cache mati duluan, koneksi mati terakhir.
Menjatuhkan Nilai Lebih Awal #
Kadang sumber daya harus dilepas jauh sebelum scope usai — berkas besar selesai dibaca di tengah fungsi, memori harus dikembalikan lebih cepat. Rust menyediakan fungsi std::mem::drop untuk itu:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!(" mematikan {}", self.name);
}
}
fn main() {
let _file = Resource { name: String::from("berkas log") };
println!("sebelum drop eksplisit");
drop(_file);
println!("setelah drop eksplisit");
}Output yang Dihasilkan:
sebelum drop eksplisit
mematikan berkas log
setelah drop eksplisitMari kita bedah kode di atas:
- Baris 15 (
drop(_file);): fungsidropdaristd::memmengambil kepemilikan_filelalu membiarkannya mati seketika di dalam tubuh fungsinya — methoddropmilikResourceberjalan tepat pada baris ini. Fungsi ini berada di prelude, jadi tanpausepun bisa dipanggil. - Urutan output: “mematikan berkas log” muncul di antara dua pesan lainnya, membuktikan pembersihan terjadi di tengah eksekusi
main— bukan di akhir scope. - Setelah Baris 15: variabel
_filetelah mati; pemakaian_filepada baris-baris berikutnya akan ditolak kompilator sebagai pemakaian nilai yang sudah dipindahkan.
Memanggil drop() Secara Langsung Ditolak #
Insting pemula yang wajar adalah memanggil method-nya sendiri: source.drop();. Kompilator menolak mentah-mentah:
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("mematikan {}", self.name);
}
}
fn main() {
let source = Resource { name: String::from("sumber data") };
source.drop();
println!("selesai");
}Pesan error kompilator:
error[E0040]: explicit use of destructor method
--> main.rs:14:12
|
14 | source.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
14 - source.drop();
14 + drop(source);
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0040`.Pesan E0040 menyatakan larangan “explicit destructor calls not allowed”. Larangan ini menjaga kebenaran pembersihan: jika kode bisa memanggil source.drop() secara manual, nilai source akan berada di keadaan “sudah dibersihkan tapi masih hidup” — dua kali pemanggilan atau pemakaian nilai setengah mati menjadi mungkin. Kompilator bahkan menyodorkan perbaikan siap pakai: ganti source.drop() dengan drop(source) — fungsi std::mem::drop yang baru saja kita pakai. Kepemilikan diambil alih penuh, nilai mati bersih sekali saja, dan tidak ada jendela waktu untuk kesalahan.
Ringkasan #
Trait Drop menjadikan pembersihan sumber daya sebagai bagian tak terpisahkan dari kepemilikan: kompilator menyisipkan pemanggilan fn drop(&mut self) di setiap titik keluar scope, nilai-nilai dalam satu scope mati dalam urutan kebalikan deklarasi, blok bersarang menjatuhkan isinya begitu blok ditutup, dan fungsi std::mem::drop mempercepat kematian ketika sumber daya harus dilepas lebih awal. Larangan E0040 atas pemanggilan manual menutup celah pembersihan ganda. Dengan Drop di tangan, kita kini memahami seluruh rantai tanggung jawab smart pointer — dan siap menghadapi batasan terbesar kepemilikan tunggal: satu data yang ingin dimiliki banyak pemilik sekaligus.