先日、プログラミング言語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();
CanvasOps
はCanvas
に対する操作を行うための構造体のようだ。このコードでは、CanvasOps
は操作対象のCanvas
を所有する。しかし、一連の操作を終えたあとはCanvasOps
ではなくCanvas
自体が持つメソッドを呼び出す必要がある。
単一書き込み原則
PythonやJavaのように、すべてのオブジェクトを参照を通して制御し、かつ可変性の指定をまともに行えない言語では、Canvas
への参照を誰が持っていようと関係なく、ほぼ好きなタイミングでCanvas
を操作できる。(Javaの final
やPythonの dataclass(frozen=True)
は子に伝播せず、限定的にしか機能しない)
しかし、Rustでは参照の規則として「一つの可変参照か不変な参照いくつでものどちらかを行える」が徹底されているため、オブジェクトを操作する場合は、そのオブジェクトを誰が持っているかが重要となる。
この規則を破りたい場合は、Rc
やArc
といった参照カウンタとRefCell
やMutex
による内部可変性パターン (interior mutability) を使用しなければならない。しかし、参照カウンタや内部可変性パターンは実行時オーバーヘッドが発生するため、まずはRefCell
やMutex
を使用せずに実装できないか検討すべきだろう。
解決策
以下では内部可変性を用いない実装を示す。
CanvasOps構造体を用いない
最初のコードでは、CanvasOps
にCanvas
を持たせた後に、構造体の外側でCanvas
を操作するため、Canvas
をRefCell
で包む必要があった。しかし、そもそもCanvasOps
はCanvas
を持つ必要があるだろうか。
CanvasOps
はCanvas
を操作するためのユーティリティーとしての機能しか持っていないため、操作の度にCanvas
の可変参照を引数で与えた方が単純である。また、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の参照の規則に文句を言いたくなったら設計を見直してみよう。