12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustの参照の規則に文句を言いたくなったら設計を見直してみよう

Last updated at Posted at 2023-08-27

先日、プログラミング言語Rustについての以下の投稿を目にした。

以下に省略したコードを載せる。

最初のコード
struct CanvasOps {
    canvas_rc: Rc<RefCell<Canvas>>,
}

impl CanvasOps {
    fn cls(&self) {
        let mut canvas = self.canvas_rc.borrow_mut();
        canvas.set_draw_color(...);
        canvas.clear();
    }

    fn tofu(&self) {
        let mut canvas = self.canvas_rc.borrow_mut();
        canvas.set_draw_color(...);
        canvas.fill_rect(...);
    }
}

...

let canvas_rc = Rc::new(RefCell::new(canvas));
let op = CanvasOp { canvas_rc: Rc::clone(&canvas_rc) };
op.cls();
op.show();
canvas_rc.borrow_mut().present();

CanvasOpsCanvasに対する操作を行うための構造体のようだ。このコードでは、CanvasOpsは操作対象のCanvasを所有する。しかし、一連の操作を終えたあとはCanvasOpsではなくCanvas自体が持つメソッドを呼び出す必要がある。

単一書き込み原則

PythonやJavaのように、すべてのオブジェクトを参照を通して制御し、かつ可変性の指定をまともに行えない言語では、Canvasへの参照を誰が持っていようと関係なく、ほぼ好きなタイミングでCanvasを操作できる。(Javaの final やPythonの dataclass(frozen=True) は子に伝播せず、限定的にしか機能しない)

しかし、Rustでは参照の規則として「一つの可変参照か不変な参照いくつでものどちらかを行える」が徹底されているため、オブジェクトを操作する場合は、そのオブジェクトを誰が持っているかが重要となる。

この規則を破りたい場合は、RcArcといった参照カウンタとRefCellMutexによる内部可変性パターン (interior mutability) を使用しなければならない。しかし、参照カウンタや内部可変性パターンは実行時オーバーヘッドが発生するため、まずはRefCellMutexを使用せずに実装できないか検討すべきだろう。

解決策

以下では内部可変性を用いない実装を示す。

CanvasOps構造体を用いない

最初のコードでは、CanvasOpsCanvasを持たせた後に、構造体の外側でCanvasを操作するため、CanvasRefCellで包む必要があった。しかし、そもそもCanvasOpsCanvasを持つ必要があるだろうか。

CanvasOpsCanvasを操作するためのユーティリティーとしての機能しか持っていないため、操作の度にCanvasの可変参照を引数で与えた方が単純である。また、CanvasOpsに設定が要らないのであれば、CanvasOps構造体を無くし、各メソッドはモジュールの関数にすれば十分だろう。

CanvasOpsの各メソッドをモジュールの関数にしたコード
fn cls(canvas: &mut Canvas) {
    canvas.set_draw_color(...);
    canvas.clear();
}

fn tofu(canvas: &mut Canvas) {
    canvas.set_draw_color(...);
    canvas.fill_rect(...);
}

...

cls(&mut canvas);
show(&mut canvas);
canvas.present();

すべての操作をラッパーを通して行う

最初のコードのようにCanvasをラップした構造体を作りたいのであれば、すべての操作をラッパーを通して行うようにしたほうが良いだろう。
一部の操作のみ直接Canvasに対して行うということは、CanvasOpsの詳細が外に漏れているとも言えるので、あまり良い設計とは言えないだろう。

すべての操作をラッパー経由で行うコード
struct CanvasOps(Canvas);

impl CanvasOps {
    fn cls(&mut self) {
        self.0.set_draw_color(...);
        self.0.clear();
    }

    fn tofu(&mut self) {
        self.0.set_draw_color(...);
        self.0.fill_rect(...);
    }

    fn present(&mut self) {
        self.0.present();
    }
}

...

let mut wrapper = CanvasOps(canvas);
wrapper.cls();
wrapper.tofu();
wrapper.present();

トレイトによる構造体の拡張

SNSで複数の人が挙げていた解決策として、トレイトを用いてCanvasを拡張する方法がある。CanvasOpsトレイトを作り、各メソッドをCanvasに実装すると以下のようになる。

トレイトによる構造体の拡張
trait CanvasOps {
    fn cls(&mut self);
    fn tofu(&mut self);
}

impl CanvasOps for Canvas {
    fn cls(&mut self) {
        self.set_draw_color(...);
        self.clear();
    }

    fn tofu(&mut self) {
        self.set_draw_color(...);
        self.fill_rect(...);
    }
}

...

canvas.cls();
canvas.tofu();
canvas.present();

ただし、この方法は私はあまり好みではない。Canvas構造体が持つメソッドとCanvasOpsが持つメソッドの粒度が異なるからだ。トレイトを用いるなら、上述のラッパー構造体に対して実装したほうが良いように思う。

まとめ

Rustの参照の規則に文句を言いたくなったら設計を見直してみよう。

12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?