委譲が欲しくなる場面
次のようなトレイトWidgetと、それを実装するBaseWidgetがあるとします。
trait Widget {
fn size(&self) -> (u32, u32);
}
struct BaseWidget {
w: u32,
h: u32,
}
impl Widget for BaseWidget {
fn size(&self) -> (u32, u32) {
(self.w, self.h)
}
}
このBaseWidgetの機能を利用して、LabelWidgetを作成したいとします。
struct LabelWidget {
base: BaseWidget,
text: String,
}
このLabelWidgetにもWidgetトレイトを実装したいのですが、その実装はBaseWidgetのものを使いたいとします。このように所有する別オブジェクトに実装を肩代わりさせる機能を、一般的に委譲(delegation)と呼ぶようです1。Rustは現状、言語機能として明示的に委譲を扱いませんが、今回の場合はどのように書くのが良いでしょうか。一番わかり易いのは、self.baseを使ってメソッドを定義してやることです。
impl Widget for LabelWidget {
fn size(&self) -> (u32, u32) {
self.base.size()
}
}
しかし、今回のようにメソッドがsize()の1つのような場合はまだ良いのですが、メソッドが増えてきたような場合はかなり冗長になります。LabelWidgetだけでなく、他にいろいろな型に定義してやりたくなった場合も、定型的な行がどんどん増えていくことになります。
他には、Derefを用いてWidgetのトレイトオブジェクトを返す方法があります。
impl std::ops::Deref for LabelWidget {
type Target = dyn Widget;
fn deref(&self) -> &(dyn Widget + 'static) {
&self.base
}
}
これで一応、LabelWidgetの値から直接size()を呼び出せますが、LabelWidgetは実際にはWidgetを実装していません。また、Derefをこのような用途に使うのは不適切だと考えられます。
参考: Deref polymorphism
というわけで、ambassadorというクレートを利用して委譲を実現します。
ambassador
ambassadorは、トレイトの実装を委譲したい場合に必要になるボイラープレートを手続きマクロの力により削減してくれるクレートです。
ambassadorを使うと、上記のコードは以下のように書けます。
use ambassador::{Delegate, delegatable_trait};
# [delegatable_trait] // <- 委譲させたいトレイトに付ける
trait Widget {
fn size(&self) -> (u32, u32);
}
struct BaseWidget {
w: u32,
h: u32,
}
impl Widget for BaseWidget {
fn size(&self) -> (u32, u32) {
(self.w, self.h)
}
}
# [derive(Delegate)]
# [delegate(Widget, target = "base")] // <- self.baseを用いてWidgetを実装
struct LabelWidget {
base: BaseWidget,
text: String,
}
fn main() {
let label_widget = LabelWidget {
base: BaseWidget { w: 64, h: 20 },
text: "hello".into(),
};
// label_widgetからsize()が呼び出せる
assert_eq!(label_widget.size(), (64, 20));
}
ちょっとした属性を追加するだけで、LabelWidgetにWidgetトレイトが実装できました。
enumに適用する
Widgetを実装した型をメンバとするAnyWidgetというenumがあったとします。
enum AnyWidget {
Base(BaseWidget),
Label(LabelWidget),
}
どのメンバもWidgetを実装しているので、AnyWidgetにもWidgetを実装させたいところです。これを愚直に書くと以下のようになります。
impl Widget for AnyWidget {
fn size(&self) -> (u32, u32) {
match self {
AnyWidget::Base(base) => base.size(),
AnyWidget::Label(label) => label.size(),
}
}
}
match文が必要になり、非常に冗長です。これをambassadorを使って書き直すと以下のようになります。
# [derive(Delegate)]
# [delegate(Widget)] // 属性を追加するだけ
enum AnyWidget {
Base(BaseWidget),
Label(LabelWidget),
}
fn main() {
let any_widget = AnyWidget::Label(LabelWidget {
base: BaseWidget { w: 64, h: 20 },
text: "hello".into(),
});
assert_eq!(any_widget.size(), (64, 20));
}
というわけで、かなり短く書けるようになりました。
余談
Rustに委譲の機能の機能を付けよう、というのは割と昔から議論されているようで、将来的には公式で似たような機能が実装されるかもしれません。
参考(RFCsへのプルリク):
https://github.com/rust-lang/rfcs/pull/1406
https://github.com/rust-lang/rfcs/pull/2393
-
正確には転送(forwarding)と呼ぶべきかもしれません ↩