採用はこちら!

Shinonome Tech Blog

株式会社Shinonomeの技術ブログ
3 min read

Rustで構造体を他の構造体から操作する

Rustで、異なる構造体から構造体のメンバを変更する方法を解説します。必要に応じて複数回ミュータブル参照を得るにはどうしたらよいのでしょうか?全体ソースコードも掲載します。

こんにちは!最近セミナーに向けてバイオリンの練習を加速させているバックエンドコースの ama です。4月になり、進級できて安心しています。

実現したいこと

Rustで以下のようなパターンを実現したいです。

struct Master {
	  /// 変更可能な状態
	  state: State,
}

let mut master = Master::new();
let user1: User = master.login("Ex4mp13_70k3N");
user1.execute(ActionA {
	  // アクションに必要なデータ
});

master に変更可能な状態 stateを持たせ、 state をログインした User のインスタンスから遠隔的に変更することを目指します。

User はメソッド execute によって親の Master 内にある state を変更します。変更方法はトレイト Action を実装した構造体を渡すことで指定します。

💡 変更方法は1種類ではなく複数種類を定義できるようにしておきたいので、複数の構造体に共通のトレイト: Action を実装することによって実現することにしました。 Action トレイトは実際の変更動作を定義するメソッド fn modify(self, state: &mut State) の実装を強制し、構造体自体の中身にはそれぞれの変更に必要なデータを持たせておきます。

考えるべき実装内容は、構造体 User のメンバーおよびメソッドです。

実装

execute はトレイトを実装した構造体全般を受けとるので impl Trait を使えば以下のように定義できるはずです。

impl User {
    fn execute(self, action: impl Action) {
        // action.modifyをここで呼び出す
    }
}

よって、 action.modify に渡すミュータブル参照(またはミュータブル参照を得られるもの)を User はメンバとして持っておく必要があります。

Stateを複数回参照する

execute の中で action.modify のみを呼びだす場合はこれでよいですが、 execute の前後にも State を参照したり変更を行いたい場合は問題が生じます。例えばデコレータのように、 action.modify の前後で State の変更に応じた動作を加えたい場合などです。

素直に実装すると以下のようになりますが、これは Borrow Checker に引っかかります。

struct User<'state> {
    state: &'state mut State,
}

fn execute(self, action: impl Action) {
    action.modify(self.state); // ここで self.state が消費される。
    callback_after_action(self.state); // ドロップ済みの self.state は使えない。
}

少なくとも action.modify での State 変更のために State のミュータブル参照を得る必要がありますが、ミュータブル参照を得ている間はミュータブル・イミュータブル関わらず参照を得られないからです。

ミュータブル参照をクローンすることはできないため、再参照を用いて必要に応じてミュータブル参照を生成できるようにすれば解決できます。これは std::cell::Cell によって実現できます。

struct User<'state> {
    state: &'state mut Cell<State>,
}

fn execute(self, action: impl Action) {
    action.modify(self.state.get_mut()); // self.state.get_mut() のライフタイムは modify の中。
    callback_after_action(self.state.get_mut()); // 前の参照が破棄されているので再参照できる。
}

// Master のメンバも変更する必要がある。
struct Master {
    state: Cell<State>,
}

結果

ここまでをまとめると以下のようになります。

use std::cell::Cell;

struct State(String);

trait Action {
    fn modify(self, state: &mut State);
}

struct ActionA {
    new_value: String,
}

impl Action for ActionA {
    fn modify(self, state: &mut State) {
        state.0 = self.new_value;
    }
}

struct Master {
    state: Cell<State>,
}

impl Master {
    fn new() -> Self {
        Self {
            state: Cell::new(State(String::from("initial"))),
        }
    }

    // 今回はテーマ外のため認証しない。
    fn login(&mut self, _token: &str) -> User {
        User {
            state: &mut self.state,
        }
    }
}

struct User<'master> {
    state: &'master mut Cell<State>,
}

impl<'master> User<'master> {
    fn execute(self, action: impl Action) {
        action.modify(self.state.get_mut());

        // 変更時に出力する。ここで新たに state への参照が必要になる。
        println!("State updated: '{}'", self.state.get_mut().0);
    }
}

fn main() {
    let mut master = Master::new();

    // 確認のため初期状態の State の出力
    println!("Initial State: '{}'", master.state.get_mut().0);

    let user1: User = master.login("Ex4mp13_70k3N");

    // ActionA の実行。ActionA と user1 はドロップされる。
    user1.execute(ActionA {
        new_value: String::from("new value"),
    });
}

実行結果は以下のようになります。

Initial State: 'initial'
State updated: 'new value'

最後に

ポイントになったのは、ミュータブル参照を得ている間は、その参照を得ているオブジェクトに対してミュータビリティに関わらず他の参照を得ることができないということです。今回は Cell から必要に応じてミュータブル参照を生成することでこの制約を回避しました。

Rustは最初のうちは Borrow Checker を通すことが1つのネックになると思います。ですが、慣れると個々の仕様がどんな問題をどのようにブレイクスルーしているのかに面白みが出てくるので固有な中毒性が湧いてきます。パズルが好きな人や、技術的な事項の探求に興味がある人におすすめの言語です。安全性や速度については言うまでもありません。

関係ない話

最近次世代言語モデルが流行っていますね。筆者も Notion AI を用いてみました。この記事では、記事の内容を踏まえた全体ソースコード (「結果」の項を参照) を生成するのに使っています。便利です。

参考文献

Cell, RefCell, UnsafeCellの違いとその使い分け - Qiita