動的ディスパッチ
CoddyのRustジャーニー「Object Oriented Programming」セクションの一部 — レッスン 45/61。
<T: Summary>のようなトレイト境界を持つジェネリクスを使用すると、Rustはコンパイル時に正確な型を決定します。これは静的ディスパッチ(static dispatch)と呼ばれ、コンパイラは使用される具体的な型ごとに特化したコードを生成します。これは高速ですが、変数は1つの特定の型しか保持できないという制限があります。
実行時に異なる型を保持できる単一の変数が必要な場合はどうすればよいでしょうか?ここでトレイトオブジェクトの出番です。Box<dyn Trait>を使用すると、そのトレイトを実装する任意の型を格納できます:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}
dynキーワードは動的ディスパッチを示します。つまり、Rustはコンパイル時ではなく実行時に、どのメソッドを呼び出すかを検索します。トレイトオブジェクトはサイズが固定されていないため、Boxが必要になります。Boxは固定サイズのポインタを提供します。
こう考えてみてください。ジェネリクスは「私は型Tを扱います」と言い、一方でトレイトオブジェクトは「これができる何でも扱います」と言います。そのトレードオフは、同じ変数を通じて異なる型を扱う柔軟性と引き換えに、わずかな実行時のコストが発生することです。
チャレンジ
簡単ダイナミックディスパッチ(動的ディスパッチ)の威力を示す車両レンタルシステムを構築しましょう!まず、レンタル可能な車両ができることを定義するトレイトを作成し、次に Box<dyn Trait> を使用して、同じ変数に異なる車両タイプを格納します。
コードは2つのファイルに分けて構成します:
vehicle.rs:&selfを受け取りStringを返すrental_infoメソッドを持つ、パブリックなRentableトレイトを定義します。次に、2つのパブリックな構造体を作成します:Car— パブリックなmodelフィールド(String)を持ちます。そのrental_infoは"Car: {model}"を返す必要があります。Bike— パブリックなbrandフィールド(String)を持ちます。そのrental_infoは"Bike: {brand}"を返す必要があります。
main.rs: vehicle モジュールを取り込み、入力を使用してダイナミックディスパッチを実演します。最初にCarを保持するBox<dyn Rentable>変数を作成してそのレンタル情報を出力し、次に同じ変数にBikeを再代入して、再びそのレンタル情報を出力します。
ここでのポイントは、Box<dyn Rentable> 型の単一の変数が Car または Bike のいずれかを保持できることです。Rustは実行時にどちらの rental_info メソッドを呼び出すべきかを判断します。これがダイナミックディスパッチの仕組みです!
出力には両方の車両の情報が表示されるはずです:
Car: {model}
Bike: {brand}例えば、入力が "Tesla Model 3" と "Trek" の場合:
Car: Tesla Model 3
Bike: Trek2つの入力(車のモデルと自転車のブランド)を受け取ります。
チートシート
Rustはジェネリクスを用いた静的ディスパッチを使用します。コンパイラはコンパイル時に各型に対して専用のコードを生成します。これは高速ですが、変数は1つの特定の型しか保持できないことを意味します。
トレイトオブジェクトは動的ディスパッチを可能にし、実行時に単一の変数が異なる型を保持できるようにします。トレイトを実装する任意の型を格納するには、Box<dyn Trait>を使用します:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.speak()); // "Woof!"
let animal: Box<dyn Speak> = Box::new(Cat);
println!("{}", animal.speak()); // "Meow!"
}dynキーワードは動的ディスパッチを示し、メソッド呼び出しは実行時に解決されます。トレイトオブジェクトはコンパイル時にサイズが不明であるため、Boxが必要になります。
主な違い: ジェネリクスは「型T(1つの特定の型)で動作する」ことを意味し、トレイトオブジェクトは「このトレイトを実装するあらゆるもの(同じ変数を通じて複数の型)で動作する」ことを意味します。トレードオフは、柔軟性と引き換えにわずかな実行時のコストが発生することです。
自分で試してみよう
mod vehicle;
use vehicle::{Rentable, Car, Bike};
fn main() {
// 入力を読み込む
let mut car_model = String::new();
std::io::stdin().read_line(&mut car_model).expect("Failed to read line");
let car_model = car_model.trim().to_string();
let mut bike_brand = String::new();
std::io::stdin().read_line(&mut bike_brand).expect("Failed to read line");
let bike_brand = bike_brand.trim().to_string();
// TODO: Carを保持するBox<dyn Rentable>変数を作成する
// rental_info()メソッドを使用して、そのレンタル情報を表示する
// TODO: 同じ変数にBikeを保持するように再代入する
// 再度そのレンタル情報を表示する
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。