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!")
    };
}