Di Section Polimorfisme kita sudah memakai Box<dyn Trait> untuk menyimpan trait object di heap, tetapi saat itu Box sendiri sengaja tidak kita bedah — Box hanya diposisikan sebagai alat bantu untuk kepemilikan penuh. Sekarang saatnya membuka kap mesinnya. Box<T> adalah smart pointer paling sederhana di Rust: satu struktur seukuran pointer yang mengalokasikan satu nilai T di heap lalu menjadi pemilik tunggal nilai tersebut. Pola “satu pemilik, data di heap” ini menjadi fondasi bagi smart pointer lain yang akan kita temui di sepanjang section ini, mulai dari Rc yang membagikan kepemilikan hingga RefCell yang melonggarkan aturan borrow. Sebelum menyentuh yang rumit, kita perlu memahami tiga hal dasar tentang Box: bagaimana alokasi heap-nya bekerja, bagaimana isinya dibaca dan diubah, dan bagaimana kepemilikannya berpindah.
Masalah: Data yang Ikut Sibuk Berpindah #
Kita mulai dari aturan nyata yang sudah kita kenal. Setiap nilai di Rust hidup di stack atau di heap, dan setiap perpindahan kepemilikan menyalin isi nilai dari satu slot stack ke slot stack lainnya. Untuk nilai kecil seperti i32, penyalinan ini nyaris gratis. Untuk struct besar — misalnya struct laporan dengan banyak field bertipe String, array, dan angka — setiap assignment dan setiap pemanggilan fungsi yang menerima nilai itu utuh menyalin seluruh isi struct. Data String itu sendiri memang tetap satu di heap, tetapi “meja operator” di stack yang menunjuk ke sana ikut tersalin bolak-balik.
Masalah kedua muncul pada tipe yang ukurannya tidak bisa ditentukan saat kompilasi — kita akan bertemu masalah ini secara dramatis pada Chapter berikutnya lewat error E0072. Kedua masalah ini punya satu akar yang sama: nilai terpaksa “berdiri sendiri” di stack dengan ukuran penuh. Solusinya adalah memindahkan nilai ke heap dan meninggalkan satu pointer kecil sebagai perwakilan resminya — dan itulah pekerjaan Box<T>.
Mekanisme Box: Pemilik Seukuran Pointer #
Saat kita menulis Box::new(75), Rust melakukan tiga hal secara berurutan. Pertama, nilai 75 dievaluasi seperti biasa. Kedua, Rust memesan sepetak memori di heap yang cukup untuk menampung nilai bertipe i32. Ketiga, nilai tersebut dipindahkan ke petak heap itu, dan Box::new mengembalikan sebuah Box<i32> berupa pointer menuju petak tersebut. Yang tersimpan di stack hanyalah pointer seukuran satu word — 8 byte pada arsitektur 64-bit — terlepas dari seberapa besar T-nya.
Box juga pemilik yang patuh aturan ownership: saat variabel Box keluar dari scope, Rust menjalankan proses drop atas Box, dan Box membebaskan petak heap yang dikelolanya sekaligus nilai di dalamnya. Karena kepemilikan tunggal, Box tidak butuh penghitung atau pemeriksaan tambahan — pembersihannya pasti terjadi tepat satu kali. Mari kita buktikan dulu klaim “seukuran pointer” dengan pengukuran nyata:
use std::mem::size_of;
fn main() {
let nilai = Box::new(75);
println!("nilai di dalam Box: {}", nilai);
println!("ukuran Box<i32>: {} byte", size_of::<Box<i32>>());
println!("ukuran i32: {} byte", size_of::<i32>());
}Output yang Dihasilkan:
nilai di dalam Box: 75
ukuran Box<i32>: 8 byte
ukuran i32: 4 byteMari kita bedah kode di atas:
- Baris 4 (
let nilai = Box::new(75);):75dipindahkan ke heap dannilaimenjadi variabel stack berisi pointer menuju lokasi heap tersebut. Tipe variabel ini adalahBox<i32>. - Baris 6 (
println!("nilai di dalam Box: {}", nilai);): makroprintln!membaca isi heap secara otomatis tanpa perintah tambahan dari kita —Boxmenerapkan auto-deref sehingga nilai di dalamnya bisa dibaca seolah-olah nilai itu berada di stack. - Baris 7 (
size_of::<Box<i32>>()): pengukuran nyata membuktikanBox<i32>hanya selebar 8 byte, yaitu selebar satu pointer, padahal yang dibungkus cukup besar untuk menampungi32apa pun. - Baris 8 (
size_of::<i32>()): nilai telanjangi32hanya 4 byte, lebih kecil daripadaBox-nya sendiri. Bagii32, membungkus denganBoxjelas tidak efisien — kita membungkus untuk data besar atau tipe khusus, bukan untuk angka kecil.
Membaca dan Mengubah Isi Box #
Pembacaan lewat println! di atas terasa otomatis, tetapi Rust juga menyediakan operator dereference * untuk mengambil nilai di dalam Box secara eksplisit. Operator inilah yang kita pakai saat mengubah isi Box, dan operator inilah yang bekerja di balik layar pada setiap pemakaian Box yang terasa “transparan”:
fn main() {
let mut angka = Box::new(75);
*angka += 25;
println!("nilai lewat dereference: {}", *angka);
println!("nilai lewat variabel: {}", angka);
}Output yang Dihasilkan:
nilai lewat dereference: 100
nilai lewat variabel: 100Mari kita bedah kode di atas:
- Baris 2 (
let mut angka = Box::new(75);): kata kuncimutwajib ada karena isi heap yang ditunjuk akan diubah. Tanpamut, kompilator menolak assignment pada baris berikutnya. - Baris 4 (
*angka += 25;): operator*mengikuti pointer di stack menuju heap, lalu ekspresi*angkamenjadi sebutan langsung untuk nilaii32di sana. Penjumlahan+= 25mengubah angka75menjadi100di heap — bukan membuatBoxbaru. - Baris 5 (
println!("...", *angka);): operator*kembali dipakai untuk membaca nilai terbaru secara eksplisit. - Baris 6 (
println!("...", angka);): pencetakan tanpa*tetap menghasilkan100— makroprintln!menerima&Box<i32>lalu dereference secara otomatis. Dua baris terakhir ini menunjukkan dua gaya penulisan yang hasilnya identik.
Kemampuan operator * ini bukan sulap khusus Box. Operator tersebut bekerja karena Box mengimplementasikan trait Deref dan DerefMut — mekanisme yang akan kita bedah tuntas pada Chapter Deref dan DerefMut, termasuk bagaimana Box bisa dipakai di banyak tempat tanpa terasa seperti pointer.
Box sebagai Pemilik yang Berpindah #
Box adalah pemilik tunggal, sehingga seluruh aturan move dari Section Ownership berlaku apa adanya. Ketika Box dikirim ke fungsi sebagai nilai, kepemilikan petak heap ikut berpindah, dan pembersihan memori kini menjadi tanggung jawab fungsi penerima:
struct ReportData {
code: u32,
summary: String,
}
fn show_report(data: Box<ReportData>) {
println!("Laporan #{}: {}", data.code, data.summary);
}
fn main() {
let mut data = Box::new(ReportData {
code: 107,
summary: String::from("penjualan kuartal pertama"),
});
println!("kode awal: {}", data.code);
data.code = 110;
println!("kode baru: {}", data.code);
show_report(data);
}Output yang Dihasilkan:
kode awal: 107
kode baru: 110
Laporan #110: penjualan kuartal pertamaMari kita bedah kode di atas:
- Baris 1-4 (
struct ReportData { ... }): struct biasa dengan dua field —codesebesar 4 byte dansummaryberupaStringyang menunjuk data teks di heap. - Baris 6 (
fn show_report(data: Box<ReportData>)): parameter bertipeBox<ReportData>menyatakan bahwa fungsi ini menerima kepemilikan penuh, bukan pinjaman&. Pemanggil tidak perlu mengembalikan apa pun. - Baris 11-14 (
let mut data = Box::new(ReportData { ... });): seluruh structReportDatadipindahkan ke heap; variabeldatadi stack hanya menyimpan pointer 8 byte. - Baris 16 (
println!("kode awal: {}", data.code);): akses fielddata.codeberjalan langsung tanpa*— operator titik secara otomatis mengikuti pointerBox(auto-deref). - Baris 17 (
data.code = 110;): pengubahan field pun bekerja dengan gaya yang sama seperti struct biasa, padahal nilai fisiknya tinggal di heap. - Baris 20 (
show_report(data);): kepemilikanBoxberpindah ke fungsi. Setelah baris ini,datamati — pemakaiandatasetelah baris ini akan ditolak kompilator dengan error E0382 (use of moved value), persis seperti memindahkanString. - Baris 7 (
println!("Laporan #{}: ...", data.code, data.summary);):show_reportmembaca field dengan auto-deref. Saat fungsi selesai, parameterdatakeluar scope,Boxdi-drop, dan seluruh petak heap — struct beserta tekssummary— dibebaskan.
Kapan Memakai Box #
Pedoman praktis pemakaian Box<T>:
- Tipe rekursif seperti linked list atau pohon yang berisi dirinya sendiri —
Boxmemutus perhitungan ukuran tak hingga (Chapter berikutnya). - Data besar yang sering berpindah antar fungsi lewat move — memindahkan pointer 8 byte lebih murah daripada menyalin struct raksasa.
- Trait object
Box<dyn Trait>ketika tipe konkrit perlu disembunyikan — sudah kita praktikkan di Section Polimorfisme. - Nilai kembalian besar dari fungsi, agar stack pemanggil tidak menumpuk salinan sementara.
Ringkasan #
Box<T> mengalokasikan satu nilai di heap dan menempatkan pointer 8 byte di stack sebagai pemiliknya: alokasi terjadi di Box::new, pembacaan dan penulisan isi berjalan lewat operator * atau auto-deref yang transparan, dan pembersihan heap terjadi otomatis saat Box keluar scope. Box tidak mengubah aturan ownership sedikit pun — kepemilikan tetap tunggal dan perpindahannya tetap tunduk pada move semantics. Keterbatasan “satu pemilik” ini menyisakan satu pertanyaan besar: apa yang terjadi jika sebuah tipe benar-benar harus berisi dirinya sendiri, padahal ukurannya harus bisa dihitung saat kompilasi? Jawabannya adalah penggunaan Box yang paling klasik, dan kita buktikan lewat error E0072 pada Chapter berikutnya.