S-JIS[2024-10-04/2024-10-07] 変更履歴

Rust RefCell構造体

Ruststd::cell::RefCellのメモ。


概要

自分の知る限り、RefCell構造体はRc構造体と一緒に使用する。
Rcは不変な値しか保持できないが、RefCellと組み合わせると可変な値も保持できるようになる。

use std::cell::RefCell;

通常のRustではコンパイル時に借用できるかどうかの判断を行うが、
RefCellを使うと実行時に借用の判断が行われる。借用できない処理を行った場合は、実行時にパニックが発生してアプリケーションが異常終了する。


RefCellはスレッド非セーフ

RefCellはスレッドアンセーフらしいので、マルチスレッドで呼ばれる用途では使ってはいけない。[2024-10-07]

RefCellは実行時に借用できるかどうかの判定を行っているらしいので、その判定がマルチスレッドに対応していないのだと思う。


そもそもRefCellを使いたい状況とは、Rcで可変メソッド(引数が「&mut self」のメソッド)を呼びたいときだと思う。(Rc単体では不変メソッドしか呼び出せないので)
で、マルチスレッド対応にしようとして単純にRcをArcに置き換えると、「Rc<RefCell<データ型>>」という状態になる。

しかしマルチスレッド対応するなら、内側のデータ型自身もマルチスレッドに対応してないといけない。
なので、内側のデータをMutexでロックしたり、プリミティブ型ならAtomicI32等のアトミック更新可能な構造体を使用したりすることになるはずだ。

Mutexのlock()は不変メソッドであり、不変なのにロックしたオブジェクトに対する変更も出来る。なので、RefCellにする必要が無い。「Arc<Mutex<データ型>>」で行ける。
AtomicI32の内部を変更する操作(storeメソッド等)も不変メソッドだ。

内側のデータ型の各メソッド内がMutexやAtomicI32等できちんと排他制御しているなら、実はそれらのメソッドは不変メソッドで良い。
不変メソッドしか呼ばないのであれば、そもそもRefCellを使う必要も無く、(データ型全体をMutexで囲む必要も無く)「Arc<データ型>」で大丈夫だ。


RefCellのメソッド

RefCell<T>のメソッド(抜粋)。

メソッド 説明
borrow(&self) -> Ref<T> Tの不変参照を返す。 let s = RefCell::new(String::from("abc"));
let len = s.borrow().len();
borrow_mut(&self) -> RefMut<T> Tの可変参照を返す。 let s = RefCell::new(String::from("abc"));
s.borrow_mut().push_str("def");

RefCellからは、borrowメソッドで不変参照、borrow_mutメソッドで可変参照が取得できる。

取得した参照を使って、保持しているインスタンスのメソッドを呼び出したりすると思うが、不変な(内部を見るだけの)メソッドを呼び出すときはborrow()、可変な(内部を変更する)メソッドを呼び出すときはborrow_mut()を使用する。

use std::{cell::RefCell, rc::Rc};

fn main() {
    let s = Rc::new(RefCell::new(String::from("abc")));
    s.borrow_mut().push_str("def");	// Stringのpush_str()は内部を変更するメソッドなので、borrow_mut()で可変参照を取得する
    let len = s.borrow().len();    	// Stringのlen()は内部を見るだけのメソッドなので、borrow()で不変参照を取得する

    println!("len={}", len);
}

可変な(内部を変更する)メソッドを呼び出すのにborrow()を使った場合は、以下のようなコンパイルエラーになる。

    s.borrow().push_str("def");
error[E0596]: cannot borrow data in dereference of `Ref<'_, String>` as mutable
 --> src/main.rs:5:5
  |
5 |     s.borrow().push_str("def");
  |     ^^^^^^^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Ref<'_, String>`

エラーメッセージを見ても「borrow_mut()を使え」とは書かれていないが^^;、borrow()をborrow_mut()に直すのが正解。


borrowメソッドのコンパイルエラーの例

普段と同じく不変メソッドを呼び出そうとしたのに、borrow()でコンパイルエラーになったことがあった。

use std::borrow::Borrow;
use std::{cell::RefCell, rc::Rc};

fn main() {
    let s = Rc::new(RefCell::new(String::from("abc")));
    let len = s.borrow().len();	←何故か、このborrowがコンパイルエラー!

    println!("len={}", len);
}
error[E0282]: type annotations needed
 --> src/main.rs:6:17
  |
6 | let len = s.borrow().len();
  |             ^^^^^^   --- type must be known at this point
  |
help: try using a fully qualified path to specify the expected types
  |
6 |     let len = <Rc<RefCell<String>> as Borrow<Borrowed>>::borrow(&s).len();
  |               +++++++++++++++++++++++++++++++++++++++++++++++++++ ~

ヘルプとして「Borrow<Borrowed>にキャストしてごらんになったら?」と出ているけど、この通りにするともっと変なエラーになる(爆)


どうやら、RefCellのborrowメソッドと同名のメソッドがstd::borrow::Borrowトレイトにも有って、どちらを呼び出すのか決定できなくてコンパイルエラーになったらしい。

Rustでは、トレイトを実装したメソッドを呼び出す際に、呼び出す側のソースコードでもそのトレイトをuse文で宣言する必要があるらしい。
なので、逆に「use std::borrow::Borrow;」があると、そのBorrowトレイトも呼び出し候補になるということだろう。

この場合は「use std::borrow::Borrow;」を削除するのが正解。
(自分の場合、IDEの補完機能のせいで このuse文が入ってしまったのだと思う)


もし「use std::borrow::Borrow;」が本当に必要で残しておきたいなら、RefCellのborrowメソッド呼び出しを以下のようにすればいい。

        let s = Rc::new(RefCell::new(String::from("abc")));
        let len = RefCell::borrow(&s).len();

同様に、BorrowMutをインポート(use)してみると、borrow_mut()でも変なコンパイルエラーになった…。

use std::borrow::BorrowMut;
use std::{cell::RefCell, rc::Rc};

fn main() {
    let s = Rc::new(RefCell::new(String::from("abc")));
    s.borrow_mut().push_str("def");	←何故か、push_strが無いというコンパイルエラー!

    let len = s.borrow().len();
    println!("len={}", len);
}
error[E0599]: no method named `push_str` found for mutable reference `&mut Rc<RefCell<String>>` in the current scope
 --> src/main.rs:6:20
  |
6 | s.borrow_mut().push_str("def");
  |                ^^^^^^^^ method not found in `&mut Rc<RefCell<String>>`

「method not found」って出てるけど、いやちゃんとあるから!
borrow_mut()の方でエラーにならない分、悪質だな^^;

これも「use std::borrow::BorrowMut;」を削除するか、RefCellのborrow_mutメソッドを以下のようにして呼び出せば解決する。

    RefCell::borrow_mut(&s).push_str("def");

stdへ戻る / Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま