グローバルナビゲーションへ

本文へ

フッターへ

お役立ち情報Blog



安全性、速度、並行性を兼ね備えた言語と、巷でうわさの「Rust」を覗いてみる(スマートポインタ編 その2)

みなさまお久しぶりです。

今回も前回の「スマートポインタ編 その1」に引き続き、Rustのスマートポインタを掘り下げていきたいと思います。

RefCell<T>

RefCell<T>は、実行時に可変性を提供する。 Rustでは、あるデータに対して複数の不変参照を持つことができるが、可変参照は一つしか持てない。 そこで、RefCell<T>を使用すると、これらの不変性のルールを実行時に強制することができます。

  • Rustの借用のチェックはコンパイル時に行われるが、RefCell<T>では実行時に行われる(つまり、コンパイル時のチェックを無視して実行時にチェックするようにできる)
  • RefCell<T>は、外部からは不変だが、内部の値を可変にすることができる(内部可変性とよばれる)

使い方

borrow():
RefCell<T>の中のデータへの不変参照を取得する。既に可変参照が存在する場合は、実行時にpanicを起こす。

borrow_mut():
RefCell<T>の中のデータへの可変参照を取得する。既に不変参照や他の可変参照が存在する場合は、実行時にpanicを起こす。

不変参照と可変参照のルール

復習をかねて不変参照と可変参照のルールについて確認していきます。

複数の不変参照

fn main() {
    let data = vec![1, 2, 3, 4, 5];

    let a = &data;
    let b = &data;

    println!("{:?} and {:?}", a, b);
}

dataに対して2つの不変参照(aとb)を持っています。
これは問題になりません。

可変参照は一つしか持てない

 fn main() {
    let mut data = vec![1, 2, 3, 4, 5];

    let a = &mut data;
    let b = &mut data;  // error!

    println!("{:?} and {:?}", a, b);
}

data対して2つの可変参照を取得しようとしているので、コンパイルエラーになります。

不変参照と可変参照は共存できない

 fn main() fn main() {
    let mut data = vec![1, 2, 3, 4, 5];

    let a = &data;       // 不変参照
    let b = &mut data;   // 可変参照 error!

    println!("{:?} and {:?}", a, b);
}

data対して不変参照と可変参照を同時に持ってしまっているため、コンパイルエラーになります。

RefCell<T>を実際につかってみる

まずはRefCell<T>を使わないコードから見ていきます。

#[derive(Debug)]
struct DataHolder {
    data: Vec<i32>,
}

impl DataHolder {
    fn add(&mut self, value: i32) {
        self.data.push(value);
    }
}

fn main() {
    let mut holder = DataHolder { data: vec![] };

    holder.add(1);
    holder.add(2);
    holder.add(3);

    println!("{:?}", holder); // DataHolder { data: [1, 2, 3] }
}

holderを可変にして、addメソッドの第一引数も可変参照にしているので問題ありません。

続いて、holderを不変にしてみたいと思います。

use std::cell::RefCell;

#[derive(Debug)]
struct DataHolder {
    data: RefCell<Vec<i32>>,
}

impl DataHolder {
    fn add(&self, value: i32) {
        let mut data = self.data.borrow_mut();
        data.push(value);
    }
}

fn main() {
    let holder = DataHolder {
        data: RefCell::new(vec![]),
    };

    holder.add(1);
    holder.add(2);
    holder.add(3);

    println!("{:?}", holder); // DataHolder { data: RefCell { value: [1, 2, 3] } } 
}

DataHolder構造体のdataフィールドにRefCellを使っています。addメソッドの第一引数が不変参照になり、borrow_mutによって、Vec<i32>への可変参照を取得しています。
holderが不変でも、内部でVec<i32>に対しては可変の操作を提供できているのが分かります。

borrowていつ使うの?

内部可変性のためにborrow_mutを使うことは理解できるが、それでは、borrowはなんのために使うのでしょうか。

use std::cell::RefCell;

struct Cache {
    data: RefCell<Option<String>>,
}

impl Cache {
    fn fetch(&self) -> String {
        // キャッシュにデータがあればそれを返す(不変参照)
        if let Some(ref cached_data) = *self.data.borrow() {
            return cached_data.clone();
        }

        let fetched_data = "hoge".to_string();
        // キャッシュに書き込む(可変参照)
        *self.data.borrow_mut() = Some(fetched_data.clone());

        // キャッシュされたデータを返す
        fetched_data
    }
}

fn main() {
    let cache = Cache {
        data: RefCell::new(None),
    };

    println!("{}", cache.fetch()); // hoge
    println!("{}", cache.fetch()); // hoge
}
  • self.data.borrow()で、RefCellから中身のOption<String>の不変参照を取得
  • *self.data.borrow()で、参照の中身にアクセスするためにデリファレンスし、Option<String>を取得
  • if let Some(ref cached_data) = ...で、Option<String>がSomeである場合、その中身にアクセスし、refキーワードを使用して、その中身への不変の参照を取得する。この場合、cached_data&String型となる(Option<String>Stringがmoveされないようにrefを付ける)

このコードのように、値を返すときに不変にしたい場合などに使えます。

スマートポインタ編のまとめ

Box<T>,Rc<T>,RefCell<T>と見てきましたが、どの場合にどのスマートポインタを使うのかを簡単にまとめました。

Box<T>

ヒープ上にデータを確保する。 スタックよりも大きなデータや、実行時にサイズが決まるデータのために使用する。 リストなどの単純な再帰データに使う。

Rc<T>

ヒープ上にデータを確保する。 複数の不変参照を持つことが出来る。 参照カウントが0になると、データが自動的に解放される。 スレッド安全ではない。(スレッド版はArc<T>を使う) ツリーなどの複数の不変参照が必要なデータに使う。

RefCell<T>

実行時に可変性の制約をチェックすることで、内部可変性を提供する。 複数の可変参照や、同時に可変参照と不変参照を取得しようとするとパニックになる。 一つの不変参照を通じて、実際(内部)のデータを可変的に扱いたいときに使用する。(外部からは不変に見えるが、内部的では可変であるという特性を持たせることが出来る) ツリーを更新したい場合はノードを可変参照にしないといけないのでRcと共に使う。(Rc<RefCell<Node<T>>>みたいに使う)

これらを組み合わせて使うことが多いので、用法用量を守って使用していきたいと思います。

この記事を書いた人

tkr2f
tkr2f事業開発部 web application engineer
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この記事のカテゴリ

FOLLOW US

最新の情報をお届けします