Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Deref ポリモーフィズム

説明

構造体間の継承をエミュレートし、それによってメソッドを再利用するために、Deref トレイトを誤用するアンチパターンです。

ときには、Java などの OO 言語でよく見られる次のようなパターンをエミュレートしたい場合があります。

class Foo {
    void m() { ... }
}

class Bar extends Foo {}

public static void main(String[] args) {
    Bar b = new Bar();
    b.m();
}

このような振る舞いは、Deref ポリモーフィズムというアンチパターンによって実現できてしまいます。

use std::ops::Deref;

struct Foo {}

impl Foo {
    fn m(&self) {
        //..
    }
}

struct Bar {
    f: Foo,
}

impl Deref for Bar {
    type Target = Foo;
    fn deref(&self) -> &Foo {
        &self.f
    }
}

fn main() {
    let b = Bar { f: Foo {} };
    b.m();
}

Rust には構造体の継承はありません。代わりにコンポジションを使用し、BarFoo のインスタンスを含めます(このフィールドは値として保持されるため、Bar の中にインラインで格納されます。したがって、Foo にフィールドがある場合、それらはおおむね Java 版と同じようなメモリレイアウトになるでしょう(ただし、メモリレイアウトを厳密に保証したい場合は #[repr(C)] などを検討する必要があります)。

メソッド呼び出しが機能するようにするために、Bar に対して Foo をターゲットとする Deref を実装します(埋め込まれた Foo フィールドを返します)。これは、Bar をデリファレンスする(たとえば * を使う)と Foo が得られることを意味します。これはかなり奇妙です。通常、デリファレンスは T への参照から T を得るものですが、ここでは無関係な 2 つの型があります。しかし、ドット演算子は暗黙的なデリファレンスを行うため、メソッド呼び出しは Bar だけでなく Foo 上のメソッドも検索することになります。

利点

少しだけボイラープレートを省けます。たとえば次のようなものです。

impl Bar {
    fn m(&self) {
        self.f.m()
    }
}

欠点

最も重要なのは、これは意外なイディオムだということです。将来このコードを読むプログラマーは、このようなことが起こるとは予想しないでしょう。これは、Deref トレイトを、本来意図され文書化されている用途で使っているのではなく、誤用しているためです。また、ここでの仕組みが完全に暗黙的であることも理由です。

このパターンは、Java や C++ の継承のように、FooBar の間にサブタイピングを導入しません。さらに、Foo に実装されたトレイトは Bar に自動的には実装されないため、このパターンはトレイト境界によるチェックと相性が悪く、結果としてジェネリックプログラミングでも扱いにくくなります。

このパターンを使用すると、self に関して、ほとんどの OO 言語とは微妙に異なるセマンティクスになります。通常の OO 言語では、self はサブクラスへの参照のままですが、このパターンでは呼び出されたメソッドが定義されている「クラス」、つまり Foo への参照として扱われます。

最後に、このパターンは単一継承しかサポートせず、インターフェイス、クラスベースのプライバシー、その他の継承関連機能の概念もありません。そのため、Java の継承などに慣れたプログラマーにとっては、微妙に意外な体験になります。

議論

唯一の優れた代替策はありません。正確な状況によっては、トレイトを使用して再実装する方がよい場合もあれば、Foo へ手動でディスパッチするファサードメソッドを書き出す方がよい場合もあります。Rust にこれに似た継承の仕組みを導入する案は議論されていますが、安定版 Rust で利用できるようになるまでには時間がかかる可能性があります。詳細については、これらのブログ記事(記事 1記事 2)および RFC issue を参照してください。

Deref トレイトは、カスタムポインター型の実装のために設計されています。その意図は、T へのポインターを通じて T に自然にアクセスできるようにすることであり、異なる型の間で変換することではありません。これがトレイト定義によって強制されていない(おそらく強制できない)のは残念です。

Rust は、型間の明示的な変換を好みつつ、明示的な仕組みと暗黙的な仕組みの間で慎重なバランスを取ろうとしています。ドット演算子における自動デリファレンスは、エルゴノミクスの観点から暗黙的な仕組みが強く好まれるケースですが、その意図は、これを任意の型間の変換ではなく、間接参照の段階に限定することです。

関連項目