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

Rust Rc構造体

Ruststd::rc::Rcのメモ。


概要

Rc構造体は、インスタンスを複数個所で所有可能にするための構造体

use std::rc::Rc;

Rcという名前はreference counting(参照カウント)の省略形らしいが、これは内部実装を表している感じであり、重要なのは機能の方。


通常のRustのプログラミングでは、誰か一人がインスタンスの所有権を持ち、他で使いたいときはインスタンスの参照(&)を渡す。
しかし、参照をメソッドから返そうとしたり渡された参照をフィールドで保持したりしようとすると、元の所有者のライフタイムを気にしなければならず、非常に大変。
(参照は所有者が生きている間しか使用できない(有効でない)ので、ライフタイムをきっちり指定する必要がある)

Rcを使うと、インスタンスの所有権はRcが持つ。
Rcを他へ渡すときはRcのクローンを渡す。
これにより、インスタンスの所有権を気にせず、Rcをメソッドに渡したりフィールドに保持したりメソッドから返したりすることが出来る。

struct MyStruct {}

fn my_func() -> Rc<MyStruct> {
    let s: Rc<MyStruct> = Rc::new(MyStruct {});

    call1(Rc::clone(&s));	// Rcのクローンを渡す
    call2(Rc::clone(&s));	// Rcのクローンを渡す

    s	// Rcをそのまま関数から返せる
}

fn call1(s: Rc<MyStruct>) {
}

fn call2(s: Rc<MyStruct>) {
}

なお、Rcのクローン(複製)処理は、内部で参照カウンターを増やして自分自身を返すだけなので、非常に軽い。

各Rc(のクローン)が使い終わる(スコープから外れる)とRcのデストラクター(dropメソッド)が呼ばれ、その中で参照カウンターを減らす。
参照カウンターが0になったらどこからも使われていないということなので、所有していたインスタンスを解放する。

(これは、Javaのオブジェクトがどこからも参照されなくなったら(GCによって)解放されるのと似ている)

ちなみに、Rcの参照カウンターの増減は排他的な処理ではないので、シングルスレッドでしか使えないのだと思う。
(複数スレッド間で共通したい場合はArcを使う)


RcはDerefトレイトを実装しているので、Rc型の変数からRc内部のインスタンスのメソッドが呼べる。

impl MyStruct {
    fn example(&self) {
        print!("example");
    }
}

fn main() {
    let s: Rc<MyStruct> = Rc::new(MyStruct {});
    s.example();	// sはRc型の変数なのに、Rcの中のMyStructのメソッドが直接呼び出せる
}

SessionというインスタンスをSqlClientとKvsClientが保持して使うことを考えてみる。

これらの要素は、以下のような役割を負うイメージ。


Javaで例えると、以下のようなイメージ。

class Session {
}

class SqlClient {
    private final Session session;	// フィールド

    // コンストラクター
    public SqlClient(Session session) {
        this.session = session;	// 渡されたSessionをフィールドに保持
    }

    public void execute() {
        // フィールドのsessionを使って処理
    }
}

// KvsClientもSqlClientと同様
    SqlClient sqlClient;
    KvsClient kvsClient;
    {
        var session = new Session();
        sqlClient = new SqlClient(session);	// sessionを渡して内部で保持
        kvsClient = new KvsClient(session);	// sessionを渡して内部で保持
    } // session変数のスコープが終わる

    sqlClient.execute();	// SqlClient内のsessionを使って処理
    kvsClient.execute();	// KvsClient内のsessionを使って処理

Javaの場合、インスタンスの受け渡しやフィールドでの保持は自由(所有権のような概念は無い)なので、最初にSessionインスタンスを保持したsession変数のスコープが終わっても、SqlClientやKvsClient内で保持したSessionインスタンスは問題なく使える。


まずはそのままRustに置き換えてみるが、所有権の問題でコンパイルエラーになる。

struct Session {}

struct SqlClient {
    session: Session,	// フィールドでSessionインスタンスを保持
}

impl SqlClient {
    pub fn execute(&self) {
        println!("self.sessionを使って処理");
    }
}

// KvsClientもSqlClientと同様
    let sql_client;
    let kvs_client;
    {
        let session = Session {};
        sql_client = SqlClient { session: session };	// Sessionインスタンスの所有権を渡してしまっている
        kvs_client = KvsClient { session: session };	←value used here after move(所有権の移動後に値を使ってやがる!)というコンパイルエラー
    }

    sql_client.execute();
    kvs_client.execute();

最初のSqlClientインスタンス生成時にSessionインスタンスの所有権を渡してしまっているので、2つ目のKvsClientインスタンス生成時にはsession変数は使用できない。


参照(&)を使うように修正してみても、コンパイルが通らない。

struct Session {}

struct SqlClient<'a> {
    session: &'a Session,	// フィールドでSessionの参照を保持
}

impl<'a> SqlClient<'a> {
    pub fn execute(&self) {
        println!("self.sessionを使って処理");
    }
}

// KvsClientもSqlClientと同様
    let sql_client;
    let kvs_client;
    {
        let session = Session {};
        sql_client = SqlClient { session: &session };	←borrowed value does not live long enough(借用された値に十分な寿命がありませんことよ)というコンパイルエラー
        kvs_client = KvsClient { session: &session };
    } // スコープの終わり。session変数が所有しているSessionインスタンスが解放される

    sql_client.execute();	// 内部でSessionインスタンスの参照を保持しているが、所有されていたSessionインスタンスは既に解放されている
    kvs_client.execute();

Sessionインスタンスの所有者であるsession変数はブロックの終わりで解放されているのに、それへの参照を保持しているSqlClientやKvsClientが後ろの方まで生き残っているので、コンパイルエラーになる。


このように、生成されたインスタンスを他所に渡し、渡したものが生きている間は元のインスタンスも生きていてほしい(そしてその管理が難しい)場合に、Rcが利用できる。

use std::rc::Rc;

struct Session {}

struct SqlClient {
    session: Rc<Session>,	// フィールドでRc<Session>を保持
}

impl SqlClient {
    pub fn execute(&self) {
        println!("self.sessionを使って処理");
    }
}

// KvsClientもSqlClientと同様
    let sql_client;
    let kvs_client;
    {
        let session = Rc::new(Session {});	// 生成したインスタンスをRcで保持する
        sql_client = SqlClient { session: Rc::clone(&session) };	// 他所へ渡すときはRcのクローンを渡す(Rcの参照カウンターが増える)
        kvs_client = KvsClient { session: Rc::clone(&session) };
    } // session変数が終わるときにRcの参照カウンターが減る

    sql_client.execute();
    kvs_client.execute();

// sql_client・kvs_clientがスコープから外れるときに、フィールドのRcの参照カウンターが減る
// Rcの参照カウンターが0になったら、所有していたSessionインスタンスが解放される

Rc::new()で、Sessionインスタンスを所有するRcを生成する。

他の場所にSessionインスタンス(参照)を渡したい場合は、Rcのクローン(複製)を渡す。

なお、「Rc::clone(&session)」の代わりに「session.clone()」でも可能だが、Rustでは「どのメソッドを呼び出すか」の判定が「インポート(use)されたトレイト」の影響を受けるらしく、他にcloneメソッドを定義しているトレイトがインポートされていると、Rcのcloneメソッドと判断できなくてコンパイルエラーになることが有り得るようだ。
そのため、Rc::clone()を使っておく方が確実。


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