【rust】RefCellを理解する

rust

RefCellとは

スマートポインタの1つで、保持しているデータに対して、借用ルールを実行時に移します。

借用ルールとRefCell

前述のようにRefCellは借用ルールと関わりがあります。
まずは借用ルールについて簡単に説明し、実際に借用ルールを破ってみます。
その後、RefCellで借用ルールを実行時に移すとどうなるかをみていきます。

借用ルールについて

借用ルールに関しては、以下の記事に詳しく記載しています。

rustの参照と借用を理解しよう
この記事で書くこと rustの参照と借用についての説明です。 所有権については別に記事を書いていますので、必要あればご確認ください。 参照と借用 参照とは 安全なポインタ変数のことです。ポインタ変数とは、変数のアドレスが保管されている変数で...

重要な点は、「不変参照をたくさんor可変参照1つ」が借用ルールだということです。

借用ルール違反例

借用ルールを破るとどうなるのでしょうか。
実際に借用のルールを破ってみます。
以下のようなコードで借用ルールを破ることができます。
可変参照を1つ、不変参照を1つ作るため、借用ルールに違反しています。

fn main() {
    println!("compiled successfully");
    
    let mut x = 5;
    let rx = &mut x; // 可変参照
    let rx1 = &x; // 不変参照
    println!("{}",rx1);
    println!("{}",rx);
}

結果はコンパイルエラーでした。

$ cargo run
   Compiling refcell v0.1.0 (/Users/fukuda/ghq/github.com/kfuku1634/practice-rust/refcell)
error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
 --> src/main.rs:6:15
  |
5 |     let rx = &mut x;
  |              ------ mutable borrow occurs here
6 |     let rx1 = &x;
  |               ^^ immutable borrow occurs here
7 |     println!("{}",rx1);
8 |     println!("{}",rx);
  |                   -- mutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `refcell` (bin "refcell") due to previous error

RefCellを使用して借用ルールを実行時に移す

今度はRefCellを使用してみます。
RefCellを使用して、借用ルールを破るコードを書くと、借用ルールが実行時に移るのでコンパイル、ビルドはできるはずです。
それでは、実際にRefCellを使って借用ルールを破ってみます。

RefCellを用いた借用ルール違反例

以下のようなコードを書いて借用ルールを破ってみます。

use std::cell::RefCell;

fn main() {
    println!("compiled successfully");

    let x = RefCell::new(5);
    let rx = x.borrow_mut(); //可変参照
    let rx1 = x.borrow();  //不変参照
    println!("{}",rx1);
    println!("{}",rx);
}

結果、panicしました。
mainの最初のprintである「compiled successfully」は出力されました。
8行目の不変参照を作ったところで、panicします。
RefCellで可変参照と不変参照の数をカウントしており、可変参照があるのに不変参照を作ろうとするとpanicになるようです。

$ cargo run
   Compiling refcell v0.1.0 (/Users/fukuda/ghq/github.com/kfuku1634/practice-rust/refcell)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/refcell`
compiled successfully
thread 'main' panicked at 'already mutably borrowed: BorrowError', src/main.rs:8:17
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

RefCell使用の実例

AtCoderで精進しているときに、RefCellを使用するタイミングに出会いました。

こちらの問題です。
https://atcoder.jp/contests/abc353/tasks/abc353_e

公式の解説に従ってTrie木を実装している時に使用しました。
提出コードはこちらです。

Submission #55164043 - AtCoder Beginner Contest 353
AtCoder is a programming contest site for anyone from beginners to experts. We hold weekly programming contests online.

Trie木にansを持って、その値を更新しています。
最初からRefCellを使おうとは思っていませんでした。
ansをusizeで持って、処理を行おうとすると、以下のようなステップでコンパイルエラーになってしまいます。

  1. ansをusizeで持って値を更新しようとすると、solve(&self)をsolve(&mut self)に変更する必要あり。つまり、selfは可変参照である必要がある。
  2. 現在のノードから次のノードを探索するためにイテレータとして不変参照を1つ持っている。
  3. solveを再帰的に呼び出すため、可変参照を渡す必要がある。ここでルール違反になる。

RefCellを使うと、以下kのようなステップでpanicを起こさず実行できます。

  1. ansをRefCellで持つ。
  2. solveメソッドのselfは不変参照で良い。
  3. 現在のノードから次のノードを探索するためにイテレータとして不変参照を1つ持っている。
  4. solveも不変参照。
  5. ans更新時にRefCellで一時的にansの可変参照を取得。ansはここでしか参照していないので、実行時のpanicも起こらない。

借用ルールに困ったら「RefCellが使えるかも」と思い出せるようになると、さらにRustが書きやすくなるかもしれません。

コメント

タイトルとURLをコピーしました