Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Filename: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listing 15-1: Menyimpan nilai i32 di heap menggunakan sebuah box

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

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}
Listing 15-2: Usaha pertama buat mendefinisikan sebuah enum buat merepresentasikan struktur data cons list dari nilai i32

Catatan: Kita mengimplementasikan sebuah cons list yang menampung cuma nilai i32 aja 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.

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: Memakai enum List buat menyimpan list 1, 2, 3

Nilai 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
Listing 15-4: Error yang kita dapat saat mencoba mendefinisikan sebuah enum yang rekursif

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.

Sebuah Cons list yang tak terhingga: sebuah persegi panjang dengan label 'Cons' dibelah jadi dua persegi panjang yang lebih kecil. Persegi panjang kecil pertama berisi label 'i32', dan persegi panjang kecil kedua berisi label 'Cons' dan sebuah versi kecil dari persegi panjang 'Cons' yang ada di luarnya. Persegi panjang 'Cons' ini bakal terus memegang versi diri mereka sendiri yang makin mengecil sampai akhirnya sebuah persegi panjang yang berukuran wajar memegang sebuah simbol tak terhingga (infinity), yang menandakan kalau perulangan ini terjadi selamanya

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.

Filename: src/main.rs
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))))));
}
Listing 15-5: Definisi List yang memakai Box<T> biar bisa punya ukuran yang pasti diketahui

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

Sebuah persegi panjang berlabel 'Cons' dibelah jadi dua persegi panjang yang lebih kecil. Persegi panjang kecil pertama memegang label 'i32', dan persegi panjang kecil kedua memegang label 'Box' dengan satu persegi panjang di dalamnya yang mengandung label 'usize', merepresentasikan ukuran terbatas dari pointer _box_ tersebut

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.