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には構造体の継承はありません。代わりにコンポジションを使用し、 Foo のインスタンスを Bar の持ち物にします (フィールドは値なのでインラインに保持されます。したがってフィールドがあるのであれば、それらはJavaのバージョンと同じメモリレイアウトとなるでしょう (おそらくは。確実にそうしたいなら #[repr(C)] を使うべきです))。

メソッドコールを機能させるために、Bar に対して Foo をターゲット(Target)として Deref を実装します (埋め込まれた Foo フィールドを返します) 。これは (例えば * を使い) Bar を参照外しすると、 Foo が返されるということになります。これはかなり奇怪なことです。参照外しは通常 T の参照に対して T を返します。例で扱っているのは2つの関連しない型です。しかしながら、ドット演算子は暗黙的な参照外しを行うため、メソッド呼び出しは Bar に対してと同様に Foo のメソッドを検索することになります。

長所

ちょっとしたボイラーテンプレートを省けます。例えは、

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

短所

最も重要なことは、これは人を驚かせるイディオムであるということです - コードのこれを読む後のプログラマーは、このようなことが起こるとは思わないでしょう。これは私たちが Deref トレイトを、その意図 (およびドキュメント等) に反して、誤用しているからです。またこのメカニズムは完全に暗黙的なものだからです。

このパターンでは、JavaやC++の継承のようにFooBar の間に部分型を導入することはありません。さらに、 Foo で実装された trait は自動的に Bar で実装されるわけではありません。そのため、このパターンは境界チェックと相性が悪くなり、それゆえにジェネリックプログラミングとの相性も悪くなります。

このパターンを使うことは、selfに関して、ほとんどのOO言語とは微妙に異なるセマンティクスを与えます。通常はこれはサブクラスに対する参照ですが、このパターンではメソッドが定義されている「クラス」に対するものになります。

最後に、このパターンは単一継承のみをサポートし、インターフェイス、クラスベースのプライバシー、その他の継承に関連する機能はありません。そのためJavaの継承などに慣れているプログラマにとっては、微妙に驚くような経験をすることになります。

議論

唯一の良い代替案はありません。それぞれの状況によってはトレイトを再実装するのがよいかもしれませんし、または手動で Foo にディスパッチするファサードメソッドを書くのがよいかもしれません。このような継承の仕組みがRustに追加されようとしていますが、しかし安定版の Rust に至るまでに時間がかかりそうです。詳細は blog postsRFC issue を参照してください。

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

Rustは、明示的なメカニズムと暗黙的なメカニズムのバランスを慎重に取ろうとしています。そして型間の変換では明示的な変換を支持しています。ドット演算子での自動参照外しは、人間工学的に暗黙的なメカニズムを強く支持するケースですが、これは間接的な程度に限定されており、任意の型間の変換を行うものではありません。

See also

Last change: 2024-07-09, commit: 317c88e