Progrust Library.

// dictionary

トレイトオブジェクト.

トレイトオブジェクトは、dyn トレイト名と書いて「このトレイトを実装している何らかの型の値」を表す型です1。具体的な型がコンパイル時に決まっていないため、&dyn TraitやBox<dyn Trait>のように必ずポインタ越しに扱います2。メソッドを呼ぶと、値の型に応じた実装が実行時に選ばれます。これを動的ディスパッチと呼びます3。

ジェネリクスでは、ベクタ(Vec)の型引数Tが1つの型に決まるため、異なる型の値を1つのコレクションに混在させられません。トレイトオブジェクトを使うと、それが可能になります4。

trait Payment {
    fn pay(&self, amount: u32) -> String;
}

struct Cash;
struct CreditCard {
    last4: String,
}

impl Payment for Cash {
    fn pay(&self, amount: u32) -> String {
        format!("現金で{}円を支払いました", amount)
    }
}

impl Payment for CreditCard {
    fn pay(&self, amount: u32) -> String {
        format!("カード(末尾{})で{}円を支払いました", self.last4, amount)
    }
}

fn main() {
    // 異なる型を「Paymentを実装した何か」として1つのVecに入れる
    let methods: Vec<Box<dyn Payment>> = vec![
        Box::new(Cash),
        Box::new(CreditCard { last4: String::from("1234") }),
    ];

    for method in &methods {
        // どの pay が呼ばれるかは実行時に決まる
        println!("{}", method.pay(1500));
    }
}
Playgroundで開く

ジェネリクス(静的ディスパッチ)との使い分け

トレイト境界付きのジェネリクスT: Traitは、単相化によってコンパイル時に型ごとの専用コードに展開されるため、呼び先がコンパイル時に決まります(静的ディスパッチ)3。トレイトオブジェクトは呼び先を実行時に決める代わりに、その分の実行時コストがかかり、インライン化などの最適化も効きにくくなります5。

観点 T: Trait(ジェネリクス) dyn Trait(トレイトオブジェクト)
呼び先が決まるとき コンパイル時(静的ディスパッチ) 実行時(動的ディスパッチ)
異なる型の混在 不可(Vec<T>は1つの型) 可(Vec<Box<dyn Trait>>)
実行時コスト なし vtable経由の間接呼び出し
生成されるコード 使った型ごとに専用コードが生成される4 1つで済む
使えるトレイト 任意 dyn互換なトレイトのみ

扱う型が1種類に決まるなら、ジェネリクスとトレイト境界を使うのが望ましいとされています4。「実行時に型が切り替わる」「異なる型をまとめて持ちたい」場合にトレイトオブジェクトを選びます。

dyn互換性(オブジェクト安全性)

どんなトレイトでもdyn Traitにできるわけではありません。dyn Traitにできるトレイトをdyn互換(dyn compatible)なトレイトと呼びます。以前はオブジェクト安全(object safe)と呼ばれていた概念で、現在のReferenceでは名称が変わっています6。主な条件は次のとおりです6。

  • スーパートレイトもすべてdyn互換であること
  • Sizedを要求しないこと(trait T: Sizedではない)
  • 関連定数を持たず、ジェネリックな関連型も持たないこと
  • 各メソッドが、型引数を持たず、レシーバ(&self・&mut self・Box<Self>・Rc<Self>・Arc<Self>・それらのPinのいずれか)以外でSelfを使わず、async fnや戻り値位置のimpl Traitでないこと

条件を満たさないトレイトをトレイトオブジェクトにしようとすると、エラー: E0038になります。

trait Shape {
    fn area(&self) -> f64;
    fn duplicate(&self) -> Self; // 戻り値に Self を使っている
}

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 { // エラー: E0038(dyn互換でない)
    shapes.iter().map(|s| s.area()).sum()
}

Selfを返すメソッドは、呼び出し側が具体的な型を知らないので戻り値の型を決められず、トレイトオブジェクト経由では呼び出せません7。

補足

内部表現(vtable)

トレイトオブジェクトへのポインタは、「値そのものへのポインタ」と「vtable(仮想メソッドテーブル)へのポインタ」の2つを持ちます8。vtableには、その型におけるトレイトのメソッド(スーパートレイトのものを含む)それぞれの実装への関数ポインタが並んでいます8。メソッド呼び出しではvtableから関数ポインタを読み出して間接的に呼びます3。&dyn Traitが通常の参照の2倍の幅を持つ「ファットポインタ」になるのはこのためです9。

&dyn Payment
(データへのポインタ / vtableへのポインタ)

CreditCard { last4 }

vtable for CreditCard
pay → CreditCard::pay

書ける境界と省略できないdyn

dynの後ろに書けるのは、dyn互換なトレイト1つ+任意個の自動トレイト(Send・Syncなど)+ライフタイム1つまでで、?Sizedのような境界は書けません(例: dyn Payment + Send + 'static)1。ライフタイムは省略時に既定の規則で補われます10。

また、dynを省いてトレイト名だけを型として書く古い書き方は、edition 2021以降では認められません11。

Footnotes

  1. The Rust Reference - Trait objects “A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of a dyn compatible base trait plus any number of auto traits.” / “There may not be more than one non-auto trait, no more than one lifetime, and opt-out bounds (e.g. ?Sized) are not allowed.” ↩ ↩2

  2. The Rust Reference - Trait objects “Due to the opaqueness of which concrete type the value is of, trait objects are dynamically sized types. Like all DSTs, trait objects are used behind some type of pointer; for example &dyn SomeTrait or Box.” ↩

  3. The Rust Reference - Trait objects “Calling a method on a trait object results in virtual dispatch at runtime: that is, a function pointer is loaded from the trait object vtable and invoked indirectly.” / The Rust Programming Language - Trait Objects Perform Dynamic Dispatch “static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch, which is when the compiler can’t tell at compile time which method you’re calling.” ↩ ↩2 ↩3

  4. The Rust Programming Language - Using Trait Objects to Abstract over Shared Behavior “If you’ll only ever have homogeneous collections, using generics and trait bounds is preferable because the definitions will be monomorphized at compile time to use the concrete types.” ↩ ↩2 ↩3

  5. The Rust Programming Language - Trait Objects Perform Dynamic Dispatch “This lookup incurs a runtime cost that doesn’t occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method’s code, which in turn prevents some optimizations.” ↩

  6. The Rust Reference - Dyn compatibility “A dyn-compatible trait can be the base trait of a trait object.” / “Sized must not be a supertrait.” / “It must not have any associated constants.” / “It must not have any associated types with generics.” / “All supertraits must also be dyn compatible.” / “Dispatchable functions must: Not have any type parameters … Be a method that does not use Self except in the type of the receiver … Not have an opaque return type” / “Explicitly non-dispatchable functions require: Have a where Self: Sized bound” / “Note: This concept was formerly known as object safety.” ↩ ↩2 ↩3

  7. Error code E0038 “the compiler cannot predict the return type of foo() … let y = x.foo(); // What type is y?” ↩

  8. The Rust Reference - Trait objects “Each instance of a pointer to a trait object includes: a pointer to an instance of a type T that implements SomeTrait; a virtual method table, often just called a vtable, which contains, for each method of SomeTrait and its supertraits that T implements, a pointer to T’s implementation (i.e. a function pointer).” ↩ ↩2

  9. The Rust Reference - Dynamically Sized Types “Pointer types to DSTs are sized but have twice the size of pointers to sized types” / “Pointers to trait objects also store a pointer to a vtable.” ↩

  10. The Rust Reference - Default trait object lifetimes ↩

  11. The Rust Reference - Trait objects “Edition differences: Before the 2021 edition, the dyn keyword may be omitted.” ↩

この辞書が使われているページ

backlinks 6

  1. 辞書Defaultトレイト
  2. 辞書impl Trait
  3. 辞書ジェネリクス
  4. 辞書トレイト
  5. 辞書関連型
  6. 本動かして学ぶRustプログラミング問題集 › 第21章 トレイトオブジェクトと関連型