Interior mutability
Cell
Interior mutability berarti kita memiliki mutability di dalam sebuah bagian kecil dari struktur tertentu. Ingat bagaimana di Rust Anda perlu menggunakan mut
untuk mengubah variabel? Ada juga beberapa cara untuk mengubah sesuatu tanpa keyword mut
. Ini dikarenakan Rust memiliki beberapa cara untuk memungkinkan Anda mengubah value dengan aman di dalam struct yang tidak dapat diubah. Masing-masing mengikuti beberapa aturan yang memastikan bahwa perubahan value tersebut masih aman untuk dilakukan.
Pertama, mari kita lihat contoh sederhana di mana kita ingin melakukan ini. Bayangkan sebuah struct
bernama PhoneModel
yang memiliki beberapa field:
struct PhoneModel { company_name: String, model_name: String, screen_size: f32, memory: usize, date_issued: u32, on_sale: bool, } fn main() { let super_phone_3000 = PhoneModel { company_name: "YY Electronics".to_string(), model_name: "Super Phone 3000".to_string(), screen_size: 7.5, memory: 4_000_000, date_issued: 2020, on_sale: true, }; }
Tentu saja akan lebih baik apabila PhoneModel
bersifat immutable, karena kita tidak ingin datanya berubah. Contohnya, date_issued
dan screen_size
tidak pernah berubah.
Tapi di dalam struct tersebut ada satu field bernama on_sale
. Mula-mula, telefon dengan model tersebut statusnya adalah dijual (true
), namun kemudian, perusahaan tersebut memutuskan untuk tidak lagi menjualnya. Bisakah kita membuat hanya satu field menjadi mutable? Karena kita tidak ingin menuliskan let mut super_phone_3000
. Jika kita menuliskan hal tersebut, maka setiap field akan menjadi mutable.
Rust memiliki banyak cara untuk melakukan safe mutability di dalam sesuatu yang sifatnya immutable. Cara yang paling mudah untuk dilakukan adalah menggunakan Cell
. Pertama-tama, kita menuliskan use std::cell::Cell
, sehingga kita cukup menuliskan Cell
daripada menuliskan std::cell::Cell
berkali-kali.
Kemudian kita mengubah on_sale: bool
ke on_sale: Cell<bool>
. Sekarang, ia bukan lagi bool: melainkan sebuah Cell
yang menyimpan type bool
.
Cell
memiliki method yang bernama .set()
dimana Anda bisa mengganti valuenya. Kita menggunakan .set()
untuk mengubah on_sale: true
menjadi on_sale: Cell::new(true)
.
use std::cell::Cell; struct PhoneModel { company_name: String, model_name: String, screen_size: f32, memory: usize, date_issued: u32, on_sale: Cell<bool>, } fn main() { let super_phone_3000 = PhoneModel { company_name: "YY Electronics".to_string(), model_name: "Super Phone 3000".to_string(), screen_size: 7.5, memory: 4_000_000, date_issued: 2020, on_sale: Cell::new(true), }; // 10 tahun kemudian, super_phone_3000 tidak lagi diproduksi/dijual super_phone_3000.on_sale.set(false); }
Cell
bekerja pada semua jenis type, namun ia bekerja dengan baik pada Copy types yang sederhana, karena Copy type yang sederhana memberikan value, bukan references. Cell
juga memiliki method bernama get()
yang hanya bisa digunakan pada Copy type.
Type lainnya yang bisa Anda gunakan adalah RefCell
.
RefCell
RefCell
adalah cara lain untuk mengubah value tanpa perlu mendeklarasikan mut
. RefCell
adalah singkatan dari "reference cell", dan ia mirip seperti Cell
namun menggunakan reference, alih-alih menggunakan copynya.
Kita akan membuat sebuah struct bernama User
. Sejauh ini, Anda bisa melihat bahwa ia mirip seperti Cell
:
use std::cell::RefCell; #[derive(Debug)] struct User { id: u32, year_registered: u32, username: String, active: RefCell<bool>, // field-field yang lainnya } fn main() { let user_1 = User { id: 1, year_registered: 2020, username: "User 1".to_string(), active: RefCell::new(true), }; println!("{:?}", user_1.active); }
Hasil cetaknya adalah RefCell { value: true }
.
Ada banyak method yang bisa digunakan untuk RefCell
. Dua di antaranya adalah .borrow()
dan .borrow_mut()
. Dengan menggunkan method ini, Anda bisa melakukan hal yang sama seperti yang Anda lakukan terhadap &
dan &mut
. Aturannya pun tetap sama:
- Many borrows (&), aman.
- Satu mutable borrow (&mut), juga aman.
- Namun, mutable dan immutable secara bersama-sama tentu saja tidak diperbolehkan.
Jadinya, mengubah value yang berada di dalam RefCell
itu sangatlah mudah:
#![allow(unused)] fn main() { // 🚧 user_1.active.replace(false); println!("{:?}", user_1.active); }
Dan masih ada method lain untuk mengubahnya, seperti replace_with
yang menggunakan closure:
#![allow(unused)] fn main() { // 🚧 let date = 2020; user_1 .active .replace_with(|_| if date < 2000 { true } else { false }); println!("{:?}", user_1.active); }
Namun Anda haruslah berhati-hati di saat menggunakan RefCell
, karena ia akan memeriksa peminjaman di saat runtime, bukan pada saat compilation time. Runtime berarti di saat program sedang berjalan (setelah kompilasi). Sehingga, ia akan tetap melakukan kompilasi, meskipun terdapat kesalahan dalam menggunakan RefCell
:
use std::cell::RefCell; #[derive(Debug)] struct User { id: u32, year_registered: u32, username: String, active: RefCell<bool>, // Field-field lainnya } fn main() { let user_1 = User { id: 1, year_registered: 2020, username: "User 1".to_string(), active: RefCell::new(true), }; let borrow_one = user_1.active.borrow_mut(); // mutable borrow yang pertama - okay let borrow_two = user_1.active.borrow_mut(); // mutable borrow yang kedua - not okay }
Tapi di saat Anda menjalankannya, program akan langsung panic.
thread 'main' panicked at 'already borrowed: BorrowMutError', C:\Users\mithr\.rustup\toolchains\stable-x86_64-pc-windows-msvc\lib/rustlib/src/rust\src\libcore\cell.rs:877:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\rust_book.exe` (exit code: 101)
already borrowed: BorrowMutError
adalah bagian penting yang harus diperhatikan. Jadi, saat Anda menggunakan RefCell
, baiknya lakukan compile dan jalankan programnya untuk memeriksa apakah ada kesalahan.
Mutex
Mutex
adalah cara lain untuk mengubah value tanpa mendeklarasikan mut
. Mutex adalah singkatan dari mutual exclusion
, yang berarti "hanya satu di satu waktu". Ini sebabnya pula mengapa Mutex
itu aman, karena Mutex hanya memperbolehkan satu proses perubahan di satu waktu saja. Untuk melakukan ini, ia menggunakan .lock()
. Lock
ini seperti mengunci pintu dari dalam. Anda masuk ke sebuah kamar, kunci pintunya dari dalam, dan sekarang Anda bisa mengubah apapun yang berada di dalam kamar tersebut. Tidak ada orang lain yang bisa masuk ke kamar tersebut dan menghentikan Anda untuk mengubah kondisi kamar tersebut, karena Anda telah mengunci pintunya.
Mutex
lebih mudah dipahami lewat contoh dibawah ini.
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); // Membuat sebuah Mutex<i32>. Kita tidak perlu mendeklarasikannya sebagai mut let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer adalah MutexGuard // Ia harus mutable karena kita akan mengubahnya // Sekarang ia memiliki akses ke Mutex // Mari kita cetak my_mutex untuk melihatnya: println!("{:?}", my_mutex); // Ia akan mencetak "Mutex { data: <locked> }" // Sehingga sekarang kita tidak bisa mengakses data di dalam my_mutex, // kita hanya bisa mengaksesnya lewat mutex_changer println!("{:?}", mutex_changer); // Hasil cetaknya adalah 5. Mari kita ubah nilainya ke 6. *mutex_changer = 6; // mutex_changer bertype MutexGuard<i32>, jadinya kita menggunakan * untuk mengubah i32 println!("{:?}", mutex_changer); // Sekarang hasilnya adalah 6 }
Tetapi mutex_changer
tetap terkunci setelah kita melakukan perubahan. Bagaimana cara kita menghentikan pengunciannya? Mutex
akan terbuka (unlocked) secara otomatis di saat MutexGuard
keluar dari scope (goes out of scope). "Go out of scope" berarti code blocknya telah selesai. Contohnya:
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); { let mut mutex_changer = my_mutex.lock().unwrap(); *mutex_changer = 6; } // mutex_changer keluar dari scope - sekarang ia menghilang. Ia tidak lagi terkunci println!("{:?}", my_mutex); // Outputnya adalah: Mutex { data: 6 } }
Jika Anda tidak ingin menggunakan code block {}
yang berbeda, Anda bisa menggunakan std::mem::drop(mutex_changer)
. std::mem::drop
berarti "buat dia menjadi keluar dari scope".
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); let mut mutex_changer = my_mutex.lock().unwrap(); *mutex_changer = 6; std::mem::drop(mutex_changer); // drop mutex_changer - ia menghilang // dan my_mutex kembali unlocked println!("{:?}", my_mutex); // Hasilnya adalah: Mutex { data: 6 } }
Anda harus berhati-hati di saat menggunakan Mutex
, karena jika ada variabel lain yang mencoba untuk menguncinya (menggunakan lock
), ia akan menunggu:
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer yang memiliki lock-nya let mut other_mutex_changer = my_mutex.lock().unwrap(); // other_mutex_changer juga ingin melakukan lock // maka programnya akan selalu menunggu // dan menunggu // dan selamanya akan tetap menunggu. println!("This will never print..."); }
Satu method lainnya adalah try_lock()
. Ia akan sekali mencoba untuk melakukan lock, dan jika ia tidak bisa melakukan lock, ia akan menyerah. Jangan pernah mencoba menggunakan try_lock().unwrap()
, karena ia akan panic jika try_lock menyerah. Akan lebih baik untuk menggunakan if let
atau match
untuk kasus seperti ini:
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); let mut mutex_changer = my_mutex.lock().unwrap(); let mut other_mutex_changer = my_mutex.try_lock(); // try to get the lock if let Ok(value) = other_mutex_changer { println!("The MutexGuard has: {}", value) } else { println!("Didn't get the lock") } }
Juga, Anda tidak perlu untuk membuat sebuah variabel untuk mengubah Mutex
. Anda bisa melakukannya seperti ini:
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); *my_mutex.lock().unwrap() = 6; println!("{:?}", my_mutex); }
*my_mutex.lock().unwrap() = 6;
berarti "buka kunci my_mutex dan ubah nilainya menjadi 6". Tidak ada variabel yang menyimpan MutexGuardnya, sehingga Anda tidak perlu untuk menggunakan std::mem::drop
. Anda bisa melakukannya ratusan kali jika Anda mau - tidak ada masalah:
use std::sync::Mutex; fn main() { let my_mutex = Mutex::new(5); for _ in 0..100 { *my_mutex.lock().unwrap() += 1; // locks dan unlocks sebanyak 100 kali } println!("{:?}", my_mutex); }
RwLock
RwLock
kependekan dari "read write lock". Ia mirip seperti Mutex
namun juga mirip seperti RefCell
. Anda menggunakan .write().unwrap()
menggantikan .lock().unwrap()
untuk mengubah valuenya. Anda juga bisa menggunakan .read().unwrap()
untuk mendapatkan read access. Ini seperti RefCell
karena ia mengikuti aturan sebagai berikut:
- banyak variabel
.read()
, boleh, - satu variabel
.write()
, juga boleh, - tapi, lebih dari satu
.write()
, tidak boleh .read()
bersamaan dengan.write()
juga tidak boleh.
Program akan terus berjalan jika Anda mencoba menggunakan .write()
disaaat Anda tidak mendapatkan akses:
use std::sync::RwLock; fn main() { let my_rwlock = RwLock::new(5); let read1 = my_rwlock.read().unwrap(); // one .read() is fine let read2 = my_rwlock.read().unwrap(); // two .read()s is also fine println!("{:?}, {:?}", read1, read2); let write1 = my_rwlock.write().unwrap(); // uh oh, now the program will wait forever }
Jadi kita menggunakan std::mem::drop
, sama seperti yang kita lakukan pada Mutex
.
use std::sync::RwLock; use std::mem::drop; // kita akan menggunakan drop() berkali-kali fn main() { let my_rwlock = RwLock::new(5); let read1 = my_rwlock.read().unwrap(); let read2 = my_rwlock.read().unwrap(); println!("{:?}, {:?}", read1, read2); drop(read1); drop(read2); // kita drop keduanya, sehingga kita bisa menggunakan .write() sekarang let mut write1 = my_rwlock.write().unwrap(); *write1 = 6; drop(write1); println!("{:?}", my_rwlock); }
Dan Anda bisa menggunakan try_read()
dan juga try_write()
.
use std::sync::RwLock; fn main() { let my_rwlock = RwLock::new(5); let read1 = my_rwlock.read().unwrap(); let read2 = my_rwlock.read().unwrap(); if let Ok(mut number) = my_rwlock.try_write() { *number += 10; println!("Now the number is {}", number); } else { println!("Couldn't get write access, sorry!") }; }