トレイトオブジェクトとdyn
動的ディスパッチ
「トレイトオブジェクトとdyn」はCoddyKit上の無料Learn Rust Codingレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLearn Rust Coding学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Learn Rust Codingコースには全4レッスンが含まれています。
問題:異なる型の混在
トレイト境界付きのジェネリクスは、各呼び出しで1つの具体的な型を使う場合には非常に便利です。しかし、同じトレイトを共有する異なる型のコレクションを扱いたい場合はどうでしょうか。ジェネリクスでは異なる型を混在させられません。
これを解決するのがトレイトオブジェクトです。
トレイトオブジェクトとは
トレイトオブジェクトは、ポインターとdynキーワードを通して参照される値です。たとえば&dyn TraitやBox<dyn Trait>のように記述します。
これにより、実行時に複数の具体的な型を同じように扱えます。
dyn TraitのBox
Box<dyn Trait>は、そのトレイトを実装する何らかの型のヒープ上の値を所有します。具体的な型を知らなくても、トレイトのメソッドを呼び出せます。
trait Speak { fn say(&self) -> String; }
struct Dog;
impl Speak for Dog { fn say(&self) -> String { String::from("Woof") } }
fn main() {
let animal: Box<dyn Speak> = Box::new(Dog);
println!("{}", animal.say());
}トレイトオブジェクトのVec
大きな利点は、各要素がそのトレイトを実装していれば、1つのベクターにさまざまな型を格納できることです。
trait Speak { fn say(&self) -> String; }
struct Dog;
struct Cat;
impl Speak for Dog { fn say(&self) -> String { String::from("Woof") } }
impl Speak for Cat { fn say(&self) -> String { String::from("Meow") } }
fn main() {
let zoo: Vec<Box<dyn Speak>> = vec![Box::new(Dog), Box::new(Cat)];
for animal in &zoo {
println!("{}", animal.say());
}
}動的ディスパッチ
トレイトオブジェクトでは、呼び出すメソッドは、隠しテーブル(vtable)を参照して実行時に選択されます。これを動的ディスパッチと呼びます。
コストは小さな間接参照であり、利点は実行時の柔軟性です。
トレイトオブジェクトを返す関数
具体的な型が変わる場合、関数はBox<dyn Trait>を返せます。これは、実行時に型を決定するファクトリ関数で便利です。
trait Speak { fn say(&self) -> String; }
struct Dog;
struct Cat;
impl Speak for Dog { fn say(&self) -> String { String::from("Woof") } }
impl Speak for Cat { fn say(&self) -> String { String::from("Meow") } }
fn make(kind: &str) -> Box<dyn Speak> {
if kind == "dog" { Box::new(Dog) } else { Box::new(Cat) }
}
fn main() {
println!("{}", make("cat").say());
}借用したトレイトオブジェクト
所有権が不要な場合は、&dyn Traitを使って借用したトレイトオブジェクトを渡すこともできます。ヒープ割り当ては発生しません。
trait Speak { fn say(&self) -> String; }
struct Dog;
impl Speak for Dog { fn say(&self) -> String { String::from("Woof") } }
fn announce(s: &dyn Speak) {
println!("heard: {}", s.say());
}
fn main() {
let d = Dog;
announce(&d);
}オブジェクトセーフティ
すべてのトレイトをトレイトオブジェクトにできるわけではありません。トレイトはオブジェクトセーフである必要があります。大まかに言うと、メソッドはSelfを値として返したり、ジェネリック型パラメーターを使ったりできません。
&selfを受け取り、具体的な型を使うメソッドなら問題ありません。
オブジェクトセーフでない例
Selfを返すメソッドを持つトレイトはdynとして使えません。ポインターの背後にあるSelfのサイズが不明だからです。トレイトオブジェクトにするトレイトはシンプルに保ちましょう。
プラグイン形式の例
トレイトオブジェクトはプラグインシステムで威力を発揮します。ハンドラーのリストをdynの背後に格納すれば、具体的な型を意識せずにすべて実行できます。
trait Task { fn run(&self) -> i32; }
struct Add { a: i32, b: i32 }
struct Negate { x: i32 }
impl Task for Add { fn run(&self) -> i32 { self.a + self.b } }
impl Task for Negate { fn run(&self) -> i32 { -self.x } }
fn main() {
let tasks: Vec<Box<dyn Task>> = vec![Box::new(Add { a: 2, b: 3 }), Box::new(Negate { x: 7 })];
let total: i32 = tasks.iter().map(|t| t.run()).sum();
println!("total {}", total);
}dynを使う場面
異種コレクション、実行時に選択される型、または多数のジェネリック実体化によるコード肥大化の抑制が必要な場合は、トレイトオブジェクトを使います。
それ以外では、最大限の速度を得るためにジェネリックを優先します。
クイックチェック
トレイトオブジェクトについての理解度を確認しましょう。
まとめ
動的ディスパッチについて学びました。
- トレイトオブジェクトはポインターの背後で
dynを使います(Box<dyn T>、&dyn T) - 異種コレクションと、実行時に選択される型を利用できます
- メソッド呼び出しはvtableを経由します(動的ディスパッチ)
- この方法で使うには、トレイトがオブジェクトセーフである必要があります
よくある質問
「トレイトオブジェクトとdyn」レッスンは無料ですか?
はい。「トレイトオブジェクトとdyn」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Learn Rust Codingコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Learn Rust Codingコースには全4レッスンが含まれています。
「トレイトオブジェクトとdyn」で何を学びますか?
動的ディスパッチ ブラウザで直接実行するハンズオンコードでLearn Rust Codingを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Learn Rust Codingを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLearn Rust Codingは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「トレイトオブジェクトとdyn」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLearn Rust Codingレッスンでコードを書いて実行できますか?
はい。すべてのLearn Rust Codingレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- トレイトの定義
- トレイトオブジェクトとdyn
- 静的ディスパッチと動的ディスパッチ
- デフォルトメソッド