北海道苫小牧市出身の初老PGが書くブログ

永遠のプログラマを夢見る、苫小牧市出身のおじさんのちらしの裏

CellとRefCell

CellRefCell はどちらも「内側のミュータビリティ」を実現するものだが、実現方法が違う。

RefCell については、ランタイムでボローチェックしていることはよく知られている(と思う)。

    let ref_cell = RefCell::new(20i32);
    println!("ref_cell (1): {}", ref_cell.borrow());

    println!("ref_cell (2): {}", ref_cell.borrow());

    *ref_cell.borrow_mut() = 200;
    println!("ref_cell (3): {}", ref_cell.borrow());
ref_cell (1): 20
ref_cell (2): 20
ref_cell (3): 200

ランタイムでとは、文字通りコンパイラではなくランタイムで、つまりプログラムで手動で所有権を管理しているということである。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

UnsafeCell というのは、この値は「内側のミュータビリティ」として使うよという意思表示であり、要は生ポインタで処理しますっていうこと。 borrow というのが借用の数で、正の数の場合は immutable な参照の個数で、負の数の場合は mutable な参照の個数(通常は借用ルールから 1 個まで)と管理されている。

rust/cell.rs at dd4851d503f3fae0c0c742a19e0d8e6e2140bd2a · rust-lang/rust · GitHub

じゃあ、 Cell はどうかというと、こちらは copy または move によって「内側のミュータビリティ」を実現する。 immutable で定義した値について、 copy または move することによって保持する値を無理矢理変更することができる。

    let cell = Cell::new(10i32);
    println!("cell (1): {}", cell.take());

    println!("cell (2): {}", cell.take());

    cell.set(100);
    println!("cell (3): {}", cell.take());
cell (1): 10
cell (2): 0
cell (3): 100

cell (2) の出力が 0 になっていることに着目したい。これは take() されることにより、 Cell の保持する値がデフォルト値に戻ったことを意味する。変数 cell はイミュータブルで定義されているが、 println! する度に値が変わっており、きちんと「内側のミュータビリティ」を実現できていることがわかる。

実装は RefCell より遥かにシンプルなので、興味がある人は読んでみよう。保持している値が Copy なのか Default なのかで、2つの異なる API get()take() が実装されているのが面白い。

参考: Rust Cell and RefCell