內部可變性

Cell

內部可變性(Interior mutability) 的意思是在內部有一點可變性。還記得在 Rust 中,你需要用 mut 來改變變數嗎?也有一些方式能在不用 mut 這個詞時來改變它們。這是因為 Rust 有一些方式可以讓你安全地改變在不可變的結構體裡面的值。每一種方式都遵循一些規則,確保改變值時仍然是安全的。

首先,讓我們看看我們會想要這樣做的簡單範例。想像有個有很多欄位叫做 PhoneModel 的結構體:

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,
    };

}

PhoneModel 中的欄位最好是不可變的,因為我們不希望資料改變。比如說 date_issuedscreen_size 永遠不會變。

但是裡面有個欄位叫 on_sale。一個手機型號會先是銷售中 (on sale, true),但是後來公司會停賣它。我們能不能只讓這個欄位可變?因為我們不想寫 let mut super_phone_3000。如果我們這樣做,那麼每個欄位都會變得可變。

Rust 有很多方式可以讓一些不可變的東西裡面允許有一些安全的可變性,最簡單的方式叫做 Cell。首先我們宣告 use std::cell::Cell,這樣我們就可以每次只寫 Cell 而不是 std::cell::Cell

然後我們把 on_sale: bool 改成 on_sale: Cell<bool>。現在它不是 bool:它是個容納了 boolCell

Cell 有個叫做 .set() 的方法,可以用來改變值。我們用 .set()on_sale: true 改為 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 年後, super_phone_3000 不再銷售了
    super_phone_3000.on_sale.set(false);
}

Cell 適用於所有型別,但對簡單的 Copy 型別效果最好,因為它給出的是值,而不是參考。Cell 有個叫做 get() 的方法,它只對 Copy 型別有效。

另一個你可以使用的型別是 RefCell

RefCell

RefCell 是另一種無需宣告 mut 而改變值的方式。它的意思是 "reference cell",就像 Cell,但使用的是參考而不是拷貝 (copy)。

我們將建立 User 結構。到目前為止,你可以看到它與 Cell 類似:

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // 許多其它欄位
}

fn main() {
    let user_1 = User {
        id: 1,
        year_registered: 2020,
        username: "User 1".to_string(),
        active: RefCell::new(true),
    };

    println!("{:?}", user_1.active);
}

印出 RefCell { value: true }

RefCell 的方法有很多。其中兩種是 .borrow().borrow_mut()。使用這些方法,你可以做到與 &&mut 相同的事情。規則都是一樣的:

  • 可以有多個不可變借用,
  • 可以有一個可變的借用,
  • 但不行一起用可變和不可變借用。

所以改變 RefCell 中的值是非常容易的:

#![allow(unused)]
fn main() {
// 🚧
user_1.active.replace(false);
println!("{:?}", user_1.active);
}

而且還有很多其他的方法,比如 replace_with 使用的是閉包:

#![allow(unused)]
fn main() {
// 🚧
let date = 2020;

user_1
    .active
    .replace_with(|_| if date < 2000 { true } else { false });
println!("{:?}", user_1.active);
}

但是你要小心使用 RefCell,因為它是在執行時期而不是編譯時檢查借用。執行時期是指程式實際執行的時候(在編譯之後)。所以這將會被編譯,即使它是錯誤的:

use std::cell::RefCell;

#[derive(Debug)]
struct User {
    id: u32,
    year_registered: u32,
    username: String,
    active: RefCell<bool>,
    // 許多其它欄位
}

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(); // 第一個可變借用 - okay
    let borrow_two = user_1.active.borrow_mut(); // 第二個可變借用 - 不 okay
}

但如果你執行它,它就會立即恐慌。

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 是重點。所以當你使用 RefCell 時,最好去編譯執行來檢查。

Mutex

Mutex(互斥鎖) 是另一種不需要宣告 mut 就能改變數值的方式。互斥鎖的意思是 mutual exclusion,也就是"一次只能改一個"。這就是為什麼 Mutex 是安全的,因為它每次只讓一個執行緒改變它。為了做到這一點,它使用了 .lock()Lock 就像從裡面鎖上門。你進入房間,鎖上門,現在你可以在房間裡面改變東西。別人不能進來阻止你,因為你把門鎖上了。

透過範例更容易理解 Mutex

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5); // 新的 Mutex<i32>. 我們不需要加 mut
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer 是個 MutexGuard
                                                     // 它必須是 mut 因為我們將會改變它
                                                     // 現在它能存取 Mutex 了
                                                     // 讓我們印 my_mutex 來看:

    println!("{:?}", my_mutex); // 印出 "Mutex { data: <locked> }"
                                // 因此我們現在不能用 my_mutex 存取資料,
                                // 只能用 mutex_changer

    println!("{:?}", mutex_changer); // 印出 5. 讓我們改成 6.

    *mutex_changer = 6; // mutex_changer 是個 MutexGuard<i32> 所以我們用 * 來改變 i32

    println!("{:?}", mutex_changer); // 現在它說是 6
}

但是 mutex_changer 做完後還是持有著鎖。我們該如何停止呢?MutexMutexGuard 超出範圍 (out of scope) 時就會被解鎖。"超出範圍"表示該程式碼區塊已經結束執行。比如說:

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 已經超出範圍 - 現在它不見了. 不再鎖著了

    println!("{:?}", my_mutex); // 現在它會說: Mutex { data: 6 }
}

如果你不想用不同的 {} 程式碼區塊,你可以使用 std::mem::drop(mutex_changer)std::mem::drop 的意思是"讓這個超出範圍"。

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); // 丟棄 mutex_changer ── 現在不見了
                                   // 而且 my_mutex 解鎖了

    println!("{:?}", my_mutex); // 現在它會說: Mutex { data: 6 }
}

你必須小心使用 Mutex,因為如果有另一個變數試圖 lock 它,它將會等待:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);
    let mut mutex_changer = my_mutex.lock().unwrap(); // mutex_changer 拿到鎖
    let mut other_mutex_changer = my_mutex.lock().unwrap(); // other_mutex_changer 想拿鎖
                                                            // 程式正在等
                                                            // 還在等
                                                            // 又會等到永遠.

    println!("This will never print...");
}

還有一種方法是 try_lock()。然後它會試一次,如果沒能鎖上就會放棄。try_lock().unwrap() 就不做了,因為如果不成功它就會恐慌。if letmatch 比較好:

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(); // 試著拿到鎖

    if let Ok(value) = other_mutex_changer {
        println!("The MutexGuard has: {}", value)
    } else {
        println!("Didn't get the lock")
    }
}

另外,你不需要做出變數來改變 Mutex。你可以直接這樣做:

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; 的意思是"解鎖 my_mutex 並使其成為 6"。沒有任何變數來儲存它,所以你不需要呼叫 std::mem::drop。如果你願意,你可以做 100 次──這不要緊:

use std::sync::Mutex;

fn main() {
    let my_mutex = Mutex::new(5);

    for _ in 0..100 {
        *my_mutex.lock().unwrap() += 1; // 上鎖又解鎖 100 次
    }

    println!("{:?}", my_mutex);
}

RwLock

RwLock 的意思是"讀寫鎖"。它像 Mutex,但也像 RefCell。你用 .write().unwrap() 代替 .lock().unwrap() 來改變它。但你也可以用 .read().unwrap() 來獲得讀取許可權。它像是 RefCell 一樣遵循這些規則:

  • 可以有很多 .read() 變數,
  • 可以有一個 .write() 變數,
  • 但不能有多個 .write() 或同時有 .read().write()

如果在無法存取的情況下嘗試 .write()時,程式將會永遠執行:

use std::sync::RwLock;

fn main() {
    let my_rwlock = RwLock::new(5);

    let read1 = my_rwlock.read().unwrap(); // 一個 .read() 很好
    let read2 = my_rwlock.read().unwrap(); // 二個 .read() 也很好

    println!("{:?}, {:?}", read1, read2);

    let write1 = my_rwlock.write().unwrap(); // 噢哦, 現在程式會永遠等待
}

所以我們用 std::mem::drop,就像用 Mutex 一樣。

use std::sync::RwLock;
use std::mem::drop; // 我們將會使用 drop() 許多次

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); // 一起丟棄, 那現在我們才能使用 .write()

    let mut write1 = my_rwlock.write().unwrap();
    *write1 = 6;
    drop(write1);
    println!("{:?}", my_rwlock);
}

而且你也可以使用 try_read()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!")
    };
}