dyn Trait

ジェネリクスによる静的ディスパッチのためにトレイトを使うことに加えて、Rust はトレイトオブジェクトによる型消去された動的ディスパッチのためにトレイトを使うこともサポートしています。

// Copyright 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0

struct Dog {
    name: String,
    age: i8,
}
struct Cat {
    lives: i8,
}

trait Pet {
    fn talk(&self) -> String;
}

impl Pet for Dog {
    fn talk(&self) -> String {
        format!("Woof, my name is {}!", self.name)
    }
}

impl Pet for Cat {
    fn talk(&self) -> String {
        String::from("Miau!")
    }
}

// ジェネリクスと静的ディスパッチを使用する。
fn generic(pet: &impl Pet) {
    println!("Hello, who are you? {}", pet.talk());
}

// 型消去と動的ディスパッチを使用する。
fn dynamic(pet: &dyn Pet) {
    println!("Hello, who are you? {}", pet.talk());
}

fn main() {
    let cat = Cat { lives: 9 };
    let dog = Dog { name: String::from("Fido"), age: 5 };

    generic(&cat);
    generic(&dog);

    dynamic(&cat);
    dynamic(&dog);
}
  • impl Trait を含むジェネリクスは、ジェネリックが具体化されるそれぞれの異なる型ごとに、関数の特殊化されたインスタンスを作成するために単相化を使用します。これは、ジェネリック関数の内部からトレイトメソッドを呼び出す場合でも、コンパイラが完全な型情報を持っており、その型に対して使うべきトレイト実装を解決できるため、依然として静的ディスパッチが使われることを意味します。

  • dyn Trait を使う場合は、代わりに 仮想メソッドテーブル(vtable)を介した動的ディスパッチが使われます。これは、どの型の Pet が渡されるかに関係なく、単一の fn dynamic が使われることを意味します。

  • dyn Trait を使う場合、トレイトオブジェクトは何らかの間接参照の背後に置かれている必要があります。この場合は参照ですが、Box のようなスマートポインタ型を使うこともできます(これは 3 日目に実演します)。

  • 実行時には、&dyn Pet は「ファットポインタ」、つまり 2 つのポインタの組として表現されます。1 つのポインタは Pet を実装する具体的なオブジェクトを指し、もう 1 つはその型に対するトレイト実装の vtable を指します。&dyn Pet に対して talk メソッドを呼び出すとき、コンパイラは vtable 内の talk 用関数ポインタを調べ、その関数を呼び出します。その際、その関数には Dog または Cat へのポインタが渡されます。これを行うために、コンパイラは Pet の具体的な型を知っている必要はありません。

  • dyn Trait は「型消去されている」と見なされます。これは、具体的な型が何であるかについて、もはやコンパイル時の知識を持たないためです。