注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
コードがポリモーフィズムを伴う場合、実際に実行するバージョンを決定するメカニズムが必要です。 これは「ディスパッチ」(dispatch)と呼ばれます。 ディスパッチには主に静的ディスパッチと動的ディスパッチという2つの形態があります。 Rustは静的ディスパッチを支持している一方で、「トレイトオブジェクト」(trait objects)と呼ばれるメカニズムにより動的ディスパッチもサポートしています。
本章の後のために、トレイトとその実装が幾つか必要です。
単純に Foo
としましょう。
これは String
型の値を返す関数を1つ持っています。
trait Foo { fn method(&self) -> String; }
また、このトレイトを u8
と String
に実装します。
impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } }
トレイト境界を使ってこのトレイトで静的ディスパッチが出来ます。
trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something<T: Foo>(x: T) { x.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); do_something(x); do_something(y); }fn do_something<T: Foo>(x: T) { x.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); do_something(x); do_something(y); }
これはRustが u8
と String
それぞれ専用の do_something()
を作成し、それら特殊化された関数を宛てがうように呼び出しの部分を書き換えるという意味です。
(訳注: 作成された専用の do_something()
は「特殊化された関数」(specialized function)と呼ばれます)
fn do_something_u8(x: u8) { x.method(); } fn do_something_string(x: String) { x.method(); } fn main() { let x = 5u8; let y = "Hello".to_string(); do_something_u8(x); do_something_string(y); }
これは素晴らしい利点です。 呼び出される関数はコンパイル時に分かっているため、静的ディスパッチは関数呼び出しをインライン化できます。 インライン化は優れた最適化の鍵です。 静的ディスパッチは高速ですが、バイナリ的には既にあるはずの同じ関数をそれぞれの型毎に幾つもコピーするため、トレードオフとして「コードの膨張」(code bloat)が発生してしまいます。
その上、コンパイラは完璧ではなく、「最適化」したコードが遅くなってしまうこともあります。
例えば、あまりにも熱心にインライン化された関数は命令キャッシュを膨張させてしまいます(地獄の沙汰もキャッシュ次第)。
それが #[inline]
や #[inline(always)]
を慎重に使うべきである理由の1つであり、時として動的ディスパッチが静的ディスパッチよりも効率的である1つの理由なのです。
しかしながら、一般的なケースでは静的ディスパッチを使用する方が効率的であり、また、動的ディスパッチを行う薄い静的ディスパッチラッパ関数を実装することは常に可能ですが、その逆はできません。 これは静的な呼び出しの方が柔軟性に富むことを示唆しています。 標準ライブラリはこの理由から可能な限り静的ディスパッチで実装するよう心がけています。
訳注: 「動的ディスパッチを行う薄い静的ディスパッチラッパ関数を実装することは常に可能だがその逆はできない」について
静的ディスパッチはコンパイル時に定まるのに対し、動的ディスパッチは実行時に結果が分かります。従って、動的ディスパッチが伴う処理を静的ディスパッチ関数でラッピングし、半静的なディスパッチとすることは常に可能(原文で「thin」と形容しているのはこのため)ですが、動的ディスパッチで遷移した値を元に静的ディスパッチを行うことはできないと言うわけです。
Rustは「トレイトオブジェクト」と呼ばれる機能によって動的ディスパッチを提供しています。
トレイトオブジェクトは &Foo
か Box<Foo>
の様に記述され、指定されたトレイトを実装する あらゆる 型の値を保持する通常の値です。
ただし、その正確な型は実行時になって初めて判明します。
トレイトオブジェクトはトレイトを実装した具体的な型を指すポインタから キャスト する(e.g. &x as &Foo
)か、 型強制 する(e.g. &Foo
を取る関数の引数として &x
を用いる)ことで得られます。
これらトレイトオブジェクトの型強制とキャストは &mut T
を &mut Foo
へ、 Box<T>
を Box<Foo>
へ、というようにどちらもポインタに対する操作ですが、今の所はこれだけです。型強制とキャストは同一です。
この操作がまるでポインタのある型に関するコンパイラの記憶を「消去している」(erasing)ように見えることから、トレイトオブジェクトは時に「型消去」(type erasure)とも呼ばれます。
上記の例に立ち帰ると、キャストによるトレイトオブジェクトを用いた動的ディスパッチの実現にも同じトレイトが使用できます。
trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something(x: &Foo) { x.method(); } fn main() { let x = 5u8; do_something(&x as &Foo); }fn do_something(x: &Foo) { x.method(); } fn main() { let x = 5u8; do_something(&x as &Foo); }
または型強制によって、
trait Foo { fn method(&self) -> String; } impl Foo for u8 { fn method(&self) -> String { format!("u8: {}", *self) } } impl Foo for String { fn method(&self) -> String { format!("string: {}", *self) } } fn do_something(x: &Foo) { x.method(); } fn main() { let x = "Hello".to_string(); do_something(&x); }fn do_something(x: &Foo) { x.method(); } fn main() { let x = "Hello".to_string(); do_something(&x); }
トレイトオブジェクトを受け取った関数が Foo
を実装した型ごとに特殊化されることはありません。
関数は1つだけ生成され、多くの場合(とはいえ常にではありませんが)コードの膨張は少なく済みます。
しかしながら、これは低速な仮想関数の呼び出しが必要となるため、実質的にインライン化とそれに関連する最適化の機会を阻害してしまいます。
Rustはガーベジコレクタによって管理される多くの言語とは異なり、デフォルトではポインタの参照先に値を配置するようなことはしませんから、型によってサイズが違います。 関数へ引数として渡されるような値を、スタック領域へムーブしたり保存のためヒープ領域上にメモリをアロケート(デアロケートも同様)するには、コンパイル時に値のサイズを知っていることが重要となります。
Foo
のためには、 String
(24 bytes)か u8
(1 byte)もしくは Foo
(とにかくどんなサイズでも)を実装する依存クレート内の型のうちから少なくとも1つの値を格納する必要があります。
ポインタ無しで値を保存した場合、その直後の動作が正しいかどうかを保証する方法がありません。
型によって値のサイズが異なるからです。
ポインタの参照先に値を配置することはトレイトオブジェクトを渡す場合に値自体のサイズが無関係になり、ポインタのサイズのみになることを意味しています。
トレイトのメソッドはトレイトオブジェクト内にある伝統的に「vtable」(これはコンパイラによって作成、管理されます)と呼ばれる特別な関数ポインタのレコードを介して呼び出されます。
トレイトオブジェクトは単純ですが難解でもあります。 核となる表現と設計は非常に率直ですが、複雑なエラーメッセージを吐いたり、予期せぬ振る舞いが見つかったりします。
単純な例として、トレイトオブジェクトの実行時の表現から見て行きましょう。
std::raw
モジュールは複雑なビルドインの型と同じレイアウトの構造体を格納しており、 トレイトオブジェクトも含まれています 。
pub struct TraitObject { pub data: *mut (), pub vtable: *mut (), }
つまり、 &Foo
のようなトレイトオブジェクトは「data」ポインタと「vtable」ポインタから成るわけです。
dataポインタはトレイトオブジェクトが保存している(何らかの不明な型 T
の)データを指しており、vtableポインタは T
への Foo
の実装に対応するvtable(「virtual method table」)を指しています。
vtableは本質的には関数ポインタの構造体で、実装内における各メソッドの具体的な機械語の命令列を指しています。
trait_object.method()
のようなメソッド呼び出しを行うとvtableの中から適切なポインタを取り出し動的に呼び出しを行います。例えば、
struct FooVtable { destructor: fn(*mut ()), size: usize, align: usize, method: fn(*const ()) -> String, } // u8: fn call_method_on_u8(x: *const ()) -> String { // コンパイラは `x` がu8を指しているときにのみこの関数が呼ばれることを保障します let byte: &u8 = unsafe { &*(x as *const u8) }; byte.method() } static Foo_for_u8_vtable: FooVtable = FooVtable { destructor: /* コンパイラマジック */, size: 1, align: 1, // 関数ポインタへキャスト method: call_method_on_u8 as fn(*const ()) -> String, }; // String: fn call_method_on_String(x: *const ()) -> String { // コンパイラは `x` がStringを指しているときにのみこの関数が呼ばれることを保障します let string: &String = unsafe { &*(x as *const String) }; string.method() } static Foo_for_String_vtable: FooVtable = FooVtable { destructor: /* コンパイラマジック */, // この値は64bitコンピュータ向けのものです、32bitコンピュータではこの半分にします size: 24, align: 8, method: call_method_on_String as fn(*const ()) -> String, };
各vtableの destructor
フィールドはvtableが対応する型のリソースを片付ける関数を指しています。
u8
のvtableは単純な型なので何もしませんが、 String
のvtableはメモリを解放します。
このフィールドは Box<Foo>
のような自作トレイトオブジェクトのために必要であり、 Box
によるアロケートは勿論のことスコープ外に出た際に内部の型のリソースを片付けるのにも必要です。
size
及び align
フィールドは消去された型のサイズとアライメント要件を保存しています。
これらの情報はデストラクタにも組み込まれているため現時点では基本的に使われていませんが、将来、トレイトオブジェクトがより柔軟になることで使われるようになるでしょう。
例えば Foo
を実装する値を幾つか得たとします。
Foo
トレイトオブジェクトを作る、あるいは使う時のコードを明示的に書いたものは少しだけ似ているでしょう。
(型の違いを無視すればですが、どのみちただのポインタになります)
let a: String = "foo".to_string(); let x: u8 = 1; // let b: &Foo = &a; let b = TraitObject { // データを保存 data: &a, // メソッドを保存 vtable: &Foo_for_String_vtable }; // let y: &Foo = x; let y = TraitObject { // データを保存 data: &x, // メソッドを保存 vtable: &Foo_for_u8_vtable }; // b.method(); (b.vtable.method)(b.data); // y.method(); (y.vtable.method)(y.data);
全てのトレイトがトレイトオブジェクトとして使えるわけではありません。
例えば、ベクタは Clone
を実装していますが、トレイトオブジェクトを作ろうとすると、
let v = vec![1, 2, 3]; let o = &v as &Clone;
エラーが発生します。
error: cannot convert to a trait object because trait `core::clone::Clone` is not object-safe [E0038]
let o = &v as &Clone;
^~
note: the trait cannot require that `Self : Sized`
let o = &v as &Clone;
^~
エラーは Clone
が「オブジェクト安全」(object-safe)でないと言っています。
トレイトオブジェクトにできるのはオブジェクト安全なトレイトのみです。
以下の両方が真であるならばトレイトはオブジェクト安全であるといえます。
Self: Sized
を要求しないことでは何がメソッドをオブジェクト安全にするのでしょう?
各メソッドは Self: Sized
を要求するか、以下の全てを満足しなければなりません。
Self
を使ってはならないひゃー!
見ての通り、これらルールのほとんどは Self
について話しています。
「特別な状況を除いて、トレイトのメソッドで Self
を使うとオブジェクト安全ではなくなる」と考えるのが良いでしょう。