トレイトオブジェクト(dyn Trait)とは
Rustでは、トレイトを実装する型を「静的」または「動的」に扱う2つの方法があります。
- 静的ディスパッチ:ジェネリクス(
impl Trait/T: Trait)でコンパイル時に型が確定する - 動的ディスパッチ:
dyn Traitを使い、実行時に型が決まる
動的ディスパッチの仕組みが トレイトオブジェクト です。dyn Trait は「このトレイトを実装する何らかの型」を表し、ヒープ上にデータを置いて Box<dyn Trait> として使うのが一般的です。
なぜ動的ディスパッチが必要なのか
静的ディスパッチは高速ですが、「異なる型を同じコレクションにまとめて扱いたい」場合に限界があります。
trait Animal {
fn speak(&self);
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) {
println!("ワンワン!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("ニャー!");
}
}
fn main() {
// Vec<Dog> と Vec<Cat> は別の型なので混在できない
// → dyn Trait を使えば解決できる!
}
Dog と Cat は異なる型なので、そのままでは同じ Vec に入れられません。ここで dyn Trait が役立ちます。
Box<dyn Trait> で複数の型をまとめる
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Dog),
];
for animal in &animals {
animal.speak();
}
}
実行結果:
ワンワン!
ニャー!
ワンワン!
Box<dyn Animal> を使うことで、Dog でも Cat でも同じ Vec に格納できます。実行時にそれぞれの speak メソッドが正しく呼ばれる、これが動的ディスパッチです。
関数の引数・戻り値でトレイトオブジェクトを使う
引数として受け取る
fn make_sound(animal: &dyn Animal) {
animal.speak();
}
fn main() {
let dog = Dog;
let cat = Cat;
make_sound(&dog);
make_sound(&cat);
}
&dyn Animal とすることで、参照として任意の Animal 実装型を受け取れます。
戻り値として返す
ジェネリクスでは「コンパイル時に型が1つに確定する」ため、条件によって異なる型を返せません。Box<dyn Trait> を使えばこれが可能です。
fn create_animal(kind: &str) -> Box<dyn Animal> {
match kind {
"dog" => Box::new(Dog),
"cat" => Box::new(Cat),
_ => panic!("知らない動物です"),
}
}
fn main() {
let animal = create_animal("dog");
animal.speak(); // ワンワン!
let animal = create_animal("cat");
animal.speak(); // ニャー!
}
条件によって返す型を切り替えられるのは、dyn Trait ならではの強みです。
オブジェクト安全性(Object Safety)
すべてのトレイトがトレイトオブジェクトとして使えるわけではありません。Rustには オブジェクト安全(object-safe) という概念があります。
以下の条件を満たすトレイトはトレイトオブジェクトとして使えません:
- メソッドが
Selfを返す(例:Clone) - メソッドがジェネリクスパラメータを持つ
// これはオブジェクト安全ではないためコンパイルエラー
// trait MyClone {
// fn clone_self(&self) -> Self; // Self を返すのでNG
// }
// オブジェクト安全なトレイトの例
trait Describable {
fn describe(&self) -> String;
}
Self を返すメソッドは、どんな型が来るかわからない動的ディスパッチと相性が悪いため制限されています。
実践例:プラグイン的な設計パターン
トレイトオブジェクトはプラグインシステムや戦略パターンに向いています。
trait Formatter {
fn format(&self, text: &str) -> String;
}
struct UpperFormatter;
struct LowerFormatter;
struct PrefixFormatter {
prefix: String,
}
impl Formatter for UpperFormatter {
fn format(&self, text: &str) -> String {
text.to_uppercase()
}
}
impl Formatter for LowerFormatter {
fn format(&self, text: &str) -> String {
text.to_lowercase()
}
}
impl Formatter for PrefixFormatter {
fn format(&self, text: &str) -> String {
format!("{}: {}", self.prefix, text)
}
}
fn apply_formatters(text: &str, formatters: &[Box<dyn Formatter>]) -> String {
formatters.iter().fold(text.to_string(), |acc, f| f.format(&acc))
}
fn main() {
let formatters: Vec<Box<dyn Formatter>> = vec![
Box::new(PrefixFormatter { prefix: "INFO".to_string() }),
Box::new(UpperFormatter),
];
let result = apply_formatters("hello world", &formatters);
println!("{}", result); // INFO: HELLO WORLD
}
複数のフォーマッターを Vec<Box<dyn Formatter>> で管理し、順に適用しています。新しいフォーマッターを追加しても呼び出し側のコードを変更する必要がなく、拡張性の高い設計になります。
静的ディスパッチとの使い分け
| 特徴 | 静的ディスパッチ(impl Trait) | 動的ディスパッチ(dyn Trait) |
|---|---|---|
| 型の確定タイミング | コンパイル時 | 実行時 |
| 実行速度 | 速い(インライン展開) | やや遅い(vtable経由) |
| バイナリサイズ | 型ごとにコード生成 | 共通コードを使う |
| 異なる型の混在 | 不可 | 可能 |
| 戻り値に複数型 | 不可 | 可能 |
パフォーマンスを優先するなら静的ディスパッチ、柔軟な設計や異なる型の混在が必要なら動的ディスパッチを選びましょう。
まとめ
dyn Traitはトレイトオブジェクトを表し、動的ディスパッチを実現するBox<dyn Trait>で異なる型を同じコレクションにまとめられる- 関数の戻り値で条件によって型を切り替えたいときに便利
- オブジェクト安全性の制約に注意する(
Selfを返すメソッドはNG) - プラグインや戦略パターンなどの柔軟な設計に向いている
トレイトオブジェクトを使いこなすと、Rustのコードがより柔軟で拡張しやすくなります。静的ディスパッチとうまく使い分けて、設計の幅を広げてみましょう。