Memakai Box<T> buat Menunjuk ke Data di Heap
Smart pointer yang paling sederhana adalah box (kotak), yang tipenya
ditulis Box<T>. Boxes memungkinkan kita buat nyimpan data di heap
ketimbang di stack. Apa yang tersisa di stack adalah si pointer yang
menunjuk ke data di heap tersebut. Silakan merujuk lagi ke Bab 4 buat
me-review perbedaan antara stack dan heap.
Boxes tidak punya overhead performa, selain harus menyimpan data mereka di heap alih-alih di stack. Tapi mereka juga tidak punya banyak kemampuan ekstra. Kita bakal paling sering memakainya di situasi-situasi berikut:
- Saat kita punya sebuah tipe yang ukurannya tidak bisa diketahui secara pasti pas compile time dan kita mau memakai sebuah nilai dari tipe itu di konteks yang membutuhkan ukuran yang persis
- Saat kita punya jumlah data yang besar dan kita mau mentransfer kepemilikannya (ownership) tapi tetap pengen memastikan kalau datanya tidak bakal disalin (copied) saat kita melakukannya
- Saat kita pengen memiliki sebuah nilai dan kita cuma peduli kalau nilai itu adalah tipe yang mengimplementasikan trait tertentu, bukannya merupakan suatu tipe spesifik
Kita bakal mendemonstrasikan situasi pertama di “Memungkinkan Tipe Rekursif dengan Boxes”. Di kasus kedua, mentransfer kepemilikan dari data yang sangat besar bisa memakan waktu lama karena datanya bakal disalin mondar-mandir di stack. Buat meningkatkan performa di situasi ini, kita bisa menyimpan jumlah data yang besar itu di heap di dalam sebuah box. Dengan begitu, cuma data pointer yang kecil itu doang yang bakal disalin mondar-mandir di stack, sementara data yang ditunjuknya tetap diam di satu tempat di heap. Kasus ketiga dikenal sebagai trait object, dan “Memakai Trait Objects yang Mengizinkan Nilai Dari Tipe yang Berbeda-beda” di Bab 18 memang dikhususkan untuk membahas topik tersebut. Jadi, apa yang kita pelajari di sini bakal kita terapkan lagi di bagian itu!
Memakai Box<T> buat Menyimpan Data di Heap
Sebelum kita membahas kegunaan penyimpanan di heap untuk Box<T>, kita
bakal membahas sintaksnya dan gimana cara berinteraksi sama nilai yang
disimpan di dalam Box<T>.
Listing 15-1 menunjukkan gimana cara memakai box buat menyimpan nilai i32
di heap.
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
i32 di heap menggunakan sebuah boxKita mendefinisikan variabel b buat punya nilai berupa sebuah Box yang
menunjuk ke nilai 5, yang mana nilainya itu dialokasikan di heap. Program
ini bakal mencetak b = 5; di kasus ini, kita bisa mengakses data yang
ada di dalam box ini mirip kayak kalau datanya ada di stack. Sama
kayak nilai (owned value) lainnya, pas sebuah box keluar dari scope,
sebagaimana yang terjadi pada b di akhir dari main, dia bakal di-deallocate
(dihapus dari memori). Proses dealokasi ini terjadi baik untuk box-nya
(yang disimpan di stack) maupun untuk data yang ditunjuknya (yang
disimpan di heap).
Menaruh satu nilai tunggal di heap itu tidak terlalu berguna, jadi kita
tidak akan terlalu sering memakai boxes sendirian kayak gini. Membiarkan
nilai-like satu i32 tunggal ada di stack, di mana memang di situlah
mereka disimpan secara default, itu lebih tepat buat mayoritas situasi. Mari
kita lihat sebuah kasus di mana boxes memungkinkan kita buat mendefinisikan
tipe yang tidak bakal diizinkan untuk didefinisikan kalau kita tidak punya boxes.
Memungkinkan Tipe Rekursif dengan Boxes
Nilai dari sebuah tipe rekursif (recursive type) bisa punya nilai lain dari tipe yang sama sebagai bagian dari dirinya sendiri. Tipe rekursif ini menimbulkan masalah karena Rust perlu tahu saat compile time seberapa banyak ruang (space) yang dipakai sama sebuah tipe. Namun, nilai yang bersarang (nesting) di tipe rekursif ini secara teoritis bisa terus berlanjut tanpa batas, jadi Rust tidak bisa tahu berapa banyak ruang yang dibutuhkan sama nilai tersebut. Karena boxes punya ukuran yang sudah pasti diketahui, kita bisa memungkinkan tipe rekursif ini dengan menyelipkan (inserting) sebuah box ke dalam definisi tipe rekursifnya.
Sebagai contoh tipe rekursif, mari kita eksplorasi cons list. Ini adalah tipe data yang sering sekali dijumpai di bahasa pemrograman fungsional. Tipe cons list yang bakal kita definisikan itu mudah dipahami kecuali pada bagian rekursinya; maka dari itu, konsep-konsep di dalam contoh yang bakal kita kerjakan ini bakal berguna kapan pun kita masuk ke situasi yang lebih kompleks yang melibatkan tipe rekursif.
Info Lebih Lanjut soal Cons List
Sebuah cons list adalah struktur data yang berasal dari bahasa pemrograman
Lisp dan dialek-dialeknya, yang disusun dari pasangan (pairs) yang bersarang,
dan ini adalah versi Lisp dari linked list. Namanya datang dari fungsi
cons (kependekan dari fungsi construct) di Lisp yang mengonstruksi sebuah
pasangan baru dari dua argumennya. Dengan memanggil cons pada sebuah pasangan
yang terdiri dari sebuah nilai dan pasangan lain, kita bisa mengonstruksi
cons lists yang terbuat dari pasangan yang rekursif.
Misalnya, ini adalah representasi pseudocode (kode semu) dari sebuah cons list
yang berisi list 1, 2, 3 dengan setiap pasangan ditaruh di dalam tanda kurung:
(1, (2, (3, Nil)))
Tiap item di dalam cons list terdiri dari dua elemen: nilai dari item saat ini
dan item selanjutnya. Item terakhir di dalam list hanya terdiri dari sebuah
nilai bernama Nil tanpa ada item berikutnya. Sebuah cons list diproduksi dengan
memanggil fungsi cons secara rekursif. Nama standar (canonical name) untuk
menyebut kasus dasar (base case) dari rekursi ini adalah Nil. Perhatikan
bahwa ini tidak sama dengan konsep “null” atau “nil” yang dibahas di Bab 6, yang
mana itu artinya nilai yang tidak valid atau absen.
Cons list bukanlah struktur data yang sering dipakai di Rust. Kebanyakan
waktunya saat kita punya sebuah list yang berisi item-item di Rust, Vec<T>
adalah pilihan yang lebih baik buat dipakai. Namun, tipe data rekursif lainnya
yang lebih kompleks memang berguna di berbagai situasi, tapi dengan memulai
pakai cons list di bab ini, kita bisa mengeksplorasi gimana boxes memungkinkan
kita buat mendefinisikan tipe data rekursif tanpa banyak gangguan.
Listing 15-2 mengandung sebuah definisi enum untuk sebuah cons list.
Perhatikan kalau kode ini belum bisa di-compile karena tipe List tidak punya
ukuran yang pasti, yang mana bakal kita demonstrasikan.
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
i32Catatan: Kita mengimplementasikan sebuah cons list yang menampung cuma nilai
i32aja demi tujuan contoh ini. Kita bisa saja mengimplementasikannya memakai generik (generics), seperti yang sudah kita bahas di Bab 10, buat mendefinisikan tipe cons list yang bisa menyimpan nilai dari tipe apa pun.
Memakai tipe List buat menyimpan list 1, 2, 3 bakal kelihatan seperti kode
di Listing 15-3.
enum List {
Cons(i32, List),
Nil,
}
// --snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
List buat menyimpan list 1, 2, 3Nilai Cons pertama menampung 1 dan nilai List lain. Nilai List ini
adalah nilai Cons lain yang menampung 2 dan sebuah nilai List lainnya.
Nilai List ini adalah satu lagi nilai Cons yang menampung 3 dan sebuah
nilai List, yang mana pada akhirnya adalah Nil, yaitu varian non-rekursif yang
menandakan akhir dari list tersebut.
Kalau kita nyoba men-compile kode di Listing 15-3, kita dapat error yang ditunjukkan di Listing 15-4.
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Error-nya bilang kalau tipe ini “punya ukuran tak terbatas” (has infinite size).
Alasannya adalah karena kita mendefinisikan List dengan sebuah varian yang
rekursif: ia memegang nilai lain dari dirinya sendiri secara langsung. Sebagai
hasilnya, Rust tidak bisa menghitung seberapa banyak ruang yang ia butuhkan buat
menyimpan sebuah nilai List. Mari kita pecahkan masalah kenapa kita dapat
error ini. Pertama kita bakal melihat gimana cara Rust memutuskan berapa banyak
ruang yang ia butuhkan buat menyimpan nilai dari tipe non-rekursif.
Menghitung Ukuran dari Tipe Non-Rekursif
Ingat kembali enum Message yang kita definisikan di Listing 6-2 saat kita
membahas definisi enum di Bab 6:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
Buat menentukan seberapa banyak ruang yang harus dialokasikan untuk nilai
Message, Rust menelusuri setiap varian yang ada buat melihat varian mana yang
butuh ruang paling banyak. Rust melihat kalau Message::Quit tidak butuh ruang
sama sekali, Message::Move butuh ruang yang cukup buat menyimpan dua nilai i32,
dan seterusnya. Karena cuma satu varian saja yang bakal dipakai dalam satu waktu,
ruang maksimal yang bakal dibutuhkan oleh sebuah nilai Message adalah ruang yang
dibutuhkan buat menyimpan varian terbesarnya.
Bandingkan ini sama apa yang terjadi saat Rust mencoba menentukan seberapa banyak
ruang yang dibutuhkan oleh sebuah tipe rekursif kayak enum List di Listing 15-2.
Compiler mulai dengan melihat varian Cons, yang mana memegang sebuah nilai
tipe i32 dan sebuah nilai tipe List. Maka dari itu, Cons butuh jumlah ruang
yang setara dengan ukuran dari i32 ditambah sama ukuran dari List. Buat
menghitung seberapa besar memori yang dibutuhkan oleh tipe List, compiler
melihat variannya, yang dimulai dengan varian Cons. Varian Cons memegang nilai
bertipe i32 dan sebuah nilai bertipe List, dan proses ini terus berlanjut tanpa
batas, seperti yang ditunjukkan di Gambar 15-1.
Gambar 15-1: Sebuah List yang tidak terhingga yang terdiri
dari varian Cons yang tidak terhingga juga
Memakai Box<T> buat Mendapatkan Tipe Rekursif dengan Ukuran Pasti
Karena Rust tidak bisa mencari tahu seberapa banyak ruang yang harus dialokasikan buat tipe-tipe yang definisinya rekursif, compiler mengeluarkan error dengan saran yang ngebantu ini:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
Di saran ini, indirection berarti bahwa ketimbang menyimpan nilainya secara langsung, kita sebaiknya mengubah struktur datanya buat menyimpan nilai itu secara tidak langsung dengan menyimpan pointer yang merujuk ke nilai tersebut.
Karena sebuah Box<T> itu adalah sebuah pointer, Rust bakal selalu tahu
seberapa banyak ruang yang dibutuhkan oleh sebuah Box<T>: ukuran dari sebuah
pointer tidak bakal berubah tidak peduli seberapa banyak data yang dia tunjuk.
Ini artinya kita bisa menaruh sebuah Box<T> di dalam varian Cons ketimbang
menaruh nilai List lain secara langsung. Box<T> tersebut bakal menunjuk ke
nilai List berikutnya yang mana bakal berada di heap dan bukannya berada di
dalam varian Cons. Secara konsep, kita masih punya sebuah list, yang dibikin
dari list yang memegang list lainnya, tapi implementasi yang ini sekarang lebih
mirip dengan menaruh item-item tersebut bersebelahan satu sama lain ketimbang
di dalam satu sama lain.
Kita bisa mengubah definisi dari enum List di Listing 15-2 dan pemakaian dari
List di Listing 15-3 menjadi kode di Listing 15-5, yang mana sekarang bakal
bisa di-compile.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
List yang memakai Box<T> biar bisa punya ukuran yang pasti diketahuiVarian Cons butuh ukuran sebesar sebuah i32 ditambah sama ruang buat
menyimpan data pointer dari box tersebut. Varian Nil tidak menyimpan nilai
apa pun, jadi ia butuh lebih sedikit ruang di stack dibandingkan varian Cons.
Kita sekarang tahu kalau nilai List apa pun cuma bakal memakan ruang sebesar i32
ditambah dengan ukuran dari data pointer di box-nya. Dengan memakai sebuah box,
kita sudah memutuskan (broken) rantai yang tidak terhingga dan rekursif tadi,
jadi compiler sekarang bisa menghitung ukuran yang dia butuhkan buat menyimpan
nilai List. Gambar 15-2 menunjukkan seperti apa rupa dari varian Cons itu
sekarang.
Gambar 15-2: Sebuah List yang ukurannya tidak lagi tidak
terhingga karena Cons sekarang memegang sebuah Box
Boxes hanya menyediakan proses penyimpanan tidak langsung (indirection) beserta alokasi di heap; mereka tidak punya kapabilitas spesial lainnya, seperti yang bakal kita lihat di tipe-tipe smart pointer lainnya. Mereka juga tidak punya overhead performa dari kapabilitas spesial tersebut, jadi mereka bakal berguna di kasus-cases seperti cons list di mana indirection itu adalah satu-satunya fitur yang kita butuhin. Kita bakal melihat lebih banyak contoh pemakaian buat boxes di Bab 18.
Tipe Box<T> adalah sebuah smart pointer karena ia mengimplementasikan trait
Deref, yang memungkinkan nilai Box<T> buat diperlakukan seperti layaknya
sebuah referensi biasa. Pas sebuah nilai Box<T> keluar dari scope, data
di heap yang ditunjuk sama box tersebut juga bakal dibersihkan karena adanya
implementasi trait Drop. Dua trait ini bakal jadi lebih penting lagi dalam
memahami fungsionalitas yang disediakan oleh tipe-tipe smart pointer lain
yang bakal kita bahas di sisa bab ini. Mari kita eksplorasi dua trait ini secara
lebih detail.