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.
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.
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));
}
Box<T> yang mencoba berbagi kepemilikan atas list ketigaPas 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.
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));
}
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.
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));
}
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.