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

Rc<T>, Smart Pointer Reference Counted

Di mayoritas kasus, kepemilikan (ownership) itu jelas: kita tahu persis variabel mana yang memiliki suatu nilai. Namun, ada kasus di mana sebuah nilai bisa punya banyak pemilik. Misalnya, di struktur data graf (graph), banyak edges (sisi) mungkin menunjuk ke node (simpul) yang sama, dan node tersebut secara konsep dimiliki oleh semua edges yang menunjuk kepadanya. Sebuah node tidak boleh dibersihkan kecuali jika dia sudah tidak punya edges yang menunjuk kepadanya, yang artinya dia tidak punya pemilik lagi.

Kita harus mengaktifkan kepemilikan ganda (multiple ownership) secara eksplisit dengan memakai tipe Rust Rc<T>, yang merupakan singkatan dari reference counting (penghitungan referensi). Tipe Rc<T> melacak (keeps track of) jumlah referensi ke sebuah nilai buat menentukan apakah nilai tersebut masih dipakai atau tidak. Kalau ada nol referensi ke sebuah nilai, nilai tersebut bisa dibersihkan tanpa membuat referensi apa pun jadi tidak valid.

Bayangkan Rc<T> itu seperti TV di ruang keluarga. Saat ada satu orang yang masuk buat nonton TV, dia menyalakannya. Orang-orang lain bisa ikut masuk dan menonton TV. Saat orang terakhir keluar ruangan, dia bakal mematikan TV tersebut karena sudah tidak dipakai lagi. Kalau ada orang yang mematikan TV padahal orang lain masih pada nonton, pasti bakal ada keributan dari penonton-penonton yang tersisa itu!

Kita memakai tipe Rc<T> saat kita mau mengalokasikan beberapa data di heap biar bisa dibaca sama beberapa bagian dari program kita, dan saat kita tidak bisa menentukan di compile time bagian mana yang bakal paling terakhir selesai memakai datanya. Kalau kita tahu bagian mana yang bakal selesai terakhir, kita bisa aja bikin bagian itu jadi pemilik (owner) dari datanya, dan aturan ownership normal yang ditegakkan saat compile time bakal berlaku.

Perhatikan bahwa Rc<T> cuma buat dipakai di skenario single-threaded (satu thread). Saat kita ngebahas konkurensi di Bab 16, kita bakal menutupi gimana cara melakukan reference counting di program multithreaded.

Memakai Rc<T> buat Berbagi Data

Mari kembali ke contoh cons list kita di Listing 15-5. Ingat kembali kalau kita mendefinisikannya memakai Box<T>. Kali ini, kita bakal membikin dua list yang dua-duanya berbagi kepemilikan atas sebuah list ketiga. Secara konsep, ini kelihatannya mirip seperti Gambar 15-3.

Sebuah linked list dengan label 'a' menunjuk ke tiga elemen: elemen pertama berisi integer 5 dan menunjuk ke elemen kedua. Elemen kedua berisi integer 10 dan menunjuk ke elemen ketiga. Elemen ketiga berisi nilai 'Nil' yang menandakan akhir dari list; ia tidak menunjuk ke mana-mana. Sebuah linked list dengan label 'b' menunjuk ke elemen yang berisi integer 3 dan menunjuk ke elemen pertama dari list 'a'. Sebuah linked list dengan label 'c' menunjuk ke elemen yang berisi integer 4 dan juga menunjuk ke elemen pertama dari list 'a', sehingga ekor dari list 'b' dan 'c' dua-duanya adalah list 'a'

Gambar 15-3: Dua list, b dan c, berbagi kepemilikan atas sebuah list ketiga, a

Kita bakal membikin list a yang berisi 5 lalu 10. Terus kita bakal membikin dua list lagi: b yang dimulai dengan 3 dan c yang dimulai dengan 4. Baik list b maupun c nantinya bakal berlanjut ke list a pertama yang berisi 5 dan 10. Dengan kata lain, kedua list ini bakal berbagi list pertama yang berisi 5 dan 10.

Mencoba mengimplementasikan skenario ini memakai definisi dari List dengan Box<T> tidak bakal berhasil, seperti yang ditunjukkan di Listing 15-17.

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

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

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
Listing 15-17: Mendemonstrasikan kalau kita tidak diizinkan buat punya dua list memakai Box<T> yang mencoba berbagi kepemilikan atas list ketiga

Pas kita men-compile kode ini, kita bakal dapat error ini:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
 9 |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Varian-varian Cons memiliki data yang mereka pegang, jadi saat kita membikin list b, a di-move ke dalam b dan b memiliki a. Kemudian, saat kita nyoba memakai a lagi pas lagi membikin c, kita tidak diizinkan buat melakukannya karena a sudah dipindahkan.

Kita bisa saja mengubah definisi dari Cons agar memegang referensi sebagai gantinya, tapi kalau begitu kita harus menentukan parameter lifetime. Dengan menentukan parameter lifetime, kita bakal menentukan kalau setiap elemen di dalam list tersebut bakal hidup setidaknya selama keseluruhan list itu hidup. Ini sesuai buat elemen-elemen dan list-list yang ada di Listing 15-17, tapi tidak sesuai di semua skenario.

Sebagai gantinya, kita bakal mengubah definisi dari List kita agar memakai Rc<T> di tempat Box<T>, seperti yang ditunjukkan di Listing 15-18. Setiap varian Cons sekarang bakal memegang sebuah nilai dan sebuah Rc<T> yang menunjuk ke sebuah List. Saat kita membikin b, ketimbang mengambil kepemilikan dari a, kita bakal meng-clone (menyalin) Rc<List> yang lagi dipegang sama a, sehingga menaikkan jumlah referensinya dari satu jadi dua dan membiarkan a serta b berbagi kepemilikan dari data di dalam Rc<List> tersebut. Kita juga bakal meng-clone a pas membikin c, yang mana menaikkan jumlah referensinya dari dua jadi tiga. Setiap kali kita memanggil Rc::clone, reference count (jumlah referensi) ke data di dalam Rc<List> bakal naik, dan datanya tidak bakal dibersihkan kecuali ada nol referensi yang tersisa padanya.

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}
Listing 15-18: Definisi dari List yang memakai Rc<T>

Kita harus menambahkan statement use buat membawa Rc<T> ke dalam scope karena dia tidak ada di dalam prelude. Di main, kita membikin list yang memegang 5 dan 10 lalu menyimpannya di dalam Rc<List> baru di a. Terus, pas kita membikin b dan c, kita memanggil fungsi Rc::clone dan memberikan referensi ke Rc<List> di a sebagai argumennya.

Kita bisa saja memanggil a.clone() ketimbang Rc::clone(&a), tapi konvensi Rust adalah memakai Rc::clone di kasus ini. Implementasi dari Rc::clone tidak membuat deep copy (salinan dalam/penuh) dari semua datanya seperti yang dilakukan sama kebanyakan implementasi clone di tipe lain. Pemanggilan ke Rc::clone cuma menaikkan jumlah referensinya aja, yang mana tidak memakan banyak waktu. Deep copies dari data bisa memakan waktu yang lama sekali. Dengan memakai Rc::clone buat reference counting, kita bisa secara visual membedakan antara jenis clone yang berupa deep-copy dan jenis clone yang cuma menaikkan jumlah referensi. Saat lagi mencari masalah performa di kode, kita cuma perlu mempertimbangkan clones yang deep-copy aja dan bisa mengabaikan pemanggilan ke Rc::clone.

Meng-clone Rc<T> Menaikkan Reference Count

Mari kita ubah contoh pekerjaan kita di Listing 15-18 supaya kita bisa melihat gimana jumlah referensinya berubah saat kita membikin dan men-drop referensi ke Rc<List> di a.

Di Listing 15-19, kita bakal mengubah main supaya ia punya scope di dalam (inner scope) yang mengelilingi list c; dengan begitu kita bisa melihat gimana jumlah referensinya berubah saat c keluar dari scope.

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

// --snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Listing 15-19: Mencetak jumlah referensi

Di tiap titik di program di mana jumlah referensinya berubah, kita mencetak jumlah referensinya, yang mana kita dapat dengan memanggil fungsi Rc::strong_count. Fungsi ini dinamakan strong_count ketimbang count karena tipe Rc<T> juga punya sebuah weak_count; kita bakal melihat buat apa weak_count dipakai di “Mencegah Reference Cycles Memakai Weak<T>.

Kode ini mencetak yang berikut ini:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Kita bisa melihat kalau Rc<List> di a punya jumlah referensi awal sebesar 1; lalu setiap kali kita memanggil clone, jumlahnya naik 1. Saat c keluar dari scope, jumlahnya turun 1. Kita tidak perlu memanggil sebuah fungsi buat menurunkan jumlah referensinya seperti kita harus memanggil Rc::clone buat menaikkannya: implementasi dari trait Drop bakal menurunkan jumlah referensinya secara otomatis pas sebuah nilai Rc<T> keluar dari scope.

Apa yang tidak bisa kita lihat dari contoh ini adalah saat b dan kemudian a keluar dari scope di akhir main, jumlahnya menjadi 0, dan Rc<List> tersebut bakal dibersihkan seutuhnya. Memakai Rc<T> memungkinkan sebuah nilai tunggal buat punya banyak pemilik, dan penghitungan ini memastikan kalau nilainya bakal tetap valid selama salah satu dari para pemiliknya masih eksis.

Lewat referensi immutable, Rc<T> memungkinkan kita buat berbagi data di antara berbagai bagian program buat dibaca aja (reading only). Kalau Rc<T> juga mengizinkan kita buat punya banyak referensi mutable, kita mungkin bakal melanggar salah satu dari aturan borrowing yang dibahas di Bab 4: banyak referensi mutable ke tempat yang sama bisa menyebabkan data races (balapan data) dan inkonsistensi. Tapi bisa memutasi data itu kan berguna sekali! Di bagian selanjutnya, kita bakal membahas desain pola interior mutability dan tipe RefCell<T> yang bisa kita pakai bersamaan dengan sebuah Rc<T> buat menangani larangan immutability ini.