動的ディスパッチ
CoddyのRustジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 45/61。
<T: Summary>のようなトレイト境界でジェネリクスを使うと、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キーワードは動的ディスパッチを示します。Rustはコンパイル時ではなく、実行時にどのメソッドを呼び出すかを検索します。トレイトオブジェクトのサイズは既知ではないため、Boxが必要です。Boxは固定サイズのポインターを提供します。
このように考えてみてください。ジェネリクスは「型 T を扱います」と言い、トレイトオブジェクトは「これができるものなら何でも扱います」と言います。トレードオフとして、同じ変数を通じて異なる型を扱える柔軟性を得る代わりに、実行時にわずかなコストがかかります。
チャレンジ
簡単動的ディスパッチの威力を示す車両レンタルシステムを構築しましょう!レンタル可能なあらゆる車両ができることを定義するトレイトを作成し、Box<dyn Trait>を使って異なる車両型を同じ変数に格納します。
コードを2つのファイルに分けて整理します。
vehicle.rs:&selfを受け取り、Stringを返すrental_infoメソッドを持つ、publicなRentableトレイトをDefineします。次に、2つのpublicな構造体を作成します。Car: publicなmodelフィールド(String)を持ちます。そのrental_infoはCar: {model}を返すshouldです。Bike: publicなbrandフィールド(String)を持ちます。そのrental_infoはBike: {brand}を返すshouldです。
main.rs: vehicleモジュールを取り込み、入力を使って動的ディスパッチを示します。最初にCarをholdsするBox<dyn Rentable>変数をCreateし、そのレンタル情報をPrintします。その後、same変数をBikeをholdsするように再代入し、もう一度そのレンタル情報をPrintします。
ここでの魔法は、Box<dyn Rentable>型の1つの変数がCarまたはBikeのいずれかをholdsできることです。Rustは実行時にどのrental_infoメソッドを呼び出すべきかを判断します。これが実際の動的ディスパッチです!
出力には両方の車両の情報が表示されます。
Car: {model}
Bike: {brand}たとえば、入力がTesla Model 3とTrekの場合:
Car: Tesla Model 3
Bike: Trek2つの入力、つまり車のmodelと自転車のbrandを受け取ります。
自分で試してみよう
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を保持する
// レンタル情報を再度出力する
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Rustオンラインコンパイラ