Rustのトレイトオブジェクト(dyn)
トレイトのページでは、「Speak を実装している型なら何でも受け取る」関数を書きました。ジェネリクスのページでは、Rust がジェネリックな関数を型ごとに別々にコピーして作ることも見ました。どちらも、コンパイル時に型がわかっているなら最高に便利です。では、いろいろな型をひとつのリストにまとめたいときはどうすればいいでしょうか?
そのためにあるのがトレイトオブジェクト(trait object)です。音楽の先生と、生徒でいっぱいの教室を思い浮かべてください。ピアノを弾く生徒もいれば、バイオリンの生徒も、ドラムの生徒もいます。先生は、誰がどの楽器を持っているかを知らなくてもかまいません。「さあ、演奏して!」と言えば、それぞれが自分のやり方で演奏します。トレイトオブジェクトを使うと、コードでも同じことができます。「正確な型はわからないけれど、これができることはわかっている」という書き方です。
問題:ひとつの Vec に違う型を入れたい
Section titled “問題:ひとつの Vec に違う型を入れたい”図形が2種類あって、それをひとつの Vec に入れたいとしましょう。
struct Circle { radius: f64 }struct Square { side: f64 }
fn main() { let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }];}これはコンパイルできません。
error[E0308]: mismatched types --> src/main.rs:5:47 |5 | let shapes = vec![Circle { radius: 1.0 }, Square { side: 2.0 }]; | ^^^^^^^^^^^^^^^^^^^^ expected `Circle`, found `Square`エラーは「型が一致しません。Circle を期待したのに Square が見つかりました」と言っています。Vec に入れられるのは1種類の型だけです。Rust は要素をメモリ上に隙間なく並べるので、どの要素も同じサイズでなければなりません。Circle と Square は別の型なので、ふつうの Vec には一緒に入れられないのです。
Box<dyn Trait>:違う図形のリストを作る
Section titled “Box<dyn Trait>:違う図形のリストを作る”解決策は2段階です。まず、両方の図形に共通のトレイトを実装します。次に、それぞれの図形を Box に入れて、型を Box<dyn Shape> と書きます。
trait Shape { fn name(&self) -> String; fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn name(&self) -> String { String::from("円") } fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn name(&self) -> String { String::from("正方形") } fn area(&self) -> f64 { self.side * self.side }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 2.0 }), Box::new(Square { side: 3.0 }), Box::new(Circle { radius: 1.0 }), ];
for shape in &shapes { println!("{}の面積は{:.2}です", shape.name(), shape.area()); }}実行結果:
円の面積は12.57です正方形の面積は9.00です円の面積は3.14ですBox<dyn Shape> を部品ごとに読んでみましょう。
dyn Shapeは「Shapeを実装している何かの型。どの型かはプログラムを実行したときにわかる」という意味です。dynは dynamic(動的)の略です。Boxは値をヒープに置き、そこを指すポインタを持ちます(Boxはスマートポインタのページで紹介しました)。中身が何であっても、Box自体のサイズはいつも同じです。だから、これでVecも満足してくれるのです。
箱の中の値(と、「これは Circle です」と教えてくれる隠れたポインタ)をまとめたものを、トレイトオブジェクトと呼びます。
&dyn Trait:箱に入れずに借用する
Section titled “&dyn Trait:箱に入れずに借用する”いつも Box が必要なわけではありません。値がすでに別の場所にあるなら、参照でもかまいません。&dyn Shape は「Shape を実装している何かの型への、借用した参照」という意味です。
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius }}
impl Shape for Square { fn area(&self) -> f64 { self.side * self.side }}
fn print_area(shape: &dyn Shape) { println!("面積: {:.1}", shape.area());}
fn main() { let circle = Circle { radius: 1.0 }; let square = Square { side: 4.0 };
print_area(&circle); print_area(&square);
let both: Vec<&dyn Shape> = vec![&circle, &square]; println!("図形は{}個あります", both.len());}実行結果:
面積: 3.1面積: 16.0図形は2個あります使い分けのルールはシンプルです。リストが値を所有するなら Box<dyn Trait>、しばらく借りるだけなら &dyn Trait を使いましょう。
静的ディスパッチと動的ディスパッチ
Section titled “静的ディスパッチと動的ディスパッチ”「ディスパッチ」(dispatch)とは、「どのメソッドを呼ぶかを決めること」です。Rust には2つのやり方があります。
静的ディスパッチは、ジェネリクスや impl Trait を使ったときに起こります。コンパイラは正確な型を知っているので、型ごとに専用の関数のコピーを作ります(ジェネリクスのページで見たとおりです)。呼び出しは正しいメソッドへまっすぐ向かうので、とても高速です。
動的ディスパッチは、dyn Trait を使ったときに起こります。コンパイラは正確な型を知りません。そこで、トレイトオブジェクトはそれぞれ小さな隠れた表(vtable、仮想関数テーブルと呼ばれます)を持ち歩きます。この表には「この値の area はここにある」と書かれています。実行時に Rust はこの表でメソッドを探してから呼び出します。この検索の分だけ、ほんの少しコストがかかります。
// 静的ディスパッチ:Rust はこの関数を型ごとに1つずつコピーして作るfn static_area(shape: &impl Shape) -> f64 { shape.area()}
// 動的ディスパッチ:関数は1つだけで、メソッドは実行時に探されるfn dynamic_area(shape: &dyn Shape) -> f64 { shape.area()}同じ図形を渡せば、どちらの関数も同じ答えを返します。違うのは、Rust が正しい area メソッドをどうやって見つけるかだけです。では、どちらを選べばいいのでしょうか?
ジェネリクス / impl Trait を使うとき |
dyn Trait を使うとき |
|---|---|
| 1回の呼び出しで扱う型が1つに決まっている | 違う型をひとつのコレクションに混ぜたい |
| できるだけ速いコードにしたい | 型が実行時に決まる(たとえばユーザーの入力によって) |
| プログラムが少し大きくなってもかまわない | コードを小さくし、コンパイルを速くしたい |
初心者におすすめの基本方針は、まずジェネリクスか impl Trait から始めることです。「違う型をひとつのリストに入れたい」という問題にぶつかったら、そのとき dyn を使いましょう。速度の差はあまり気にしなくて大丈夫です。ほとんどのプログラムでは、違いに気づくことすらありません。
関数から Box<dyn Trait> を返す
Section titled “関数から Box<dyn Trait> を返す”関数が、どの型を返すかを実行時に決めたいこともあります。-> impl Shape と書きたくなるかもしれませんが、これは関数がいつも同じ型を返す場合にしか使えません。分岐ごとに違う型を返すなら、代わりに Box<dyn Shape> を返します。
trait Shape { fn describe(&self) -> String;}
struct Circle { radius: f64 }struct Square { side: f64 }
impl Shape for Circle { fn describe(&self) -> String { format!("半径{}の円", self.radius) }}
impl Shape for Square { fn describe(&self) -> String { format!("一辺{}の正方形", self.side) }}
fn make_shape(kind: &str, size: f64) -> Box<dyn Shape> { if kind == "circle" { Box::new(Circle { radius: size }) } else { Box::new(Square { side: size }) }}
fn main() { let a = make_shape("circle", 1.5); let b = make_shape("square", 2.0); println!("{}を作りました", a.describe()); println!("{}を作りました", b.describe());}実行結果:
半径1.5の円を作りました一辺2の正方形を作りましたこれは「ファクトリ」関数やプラグインの仕組み、設定ファイルで動きが変わるコードなどでよく使うパターンです。プログラムを実行してみるまでわからないデータによって、正確な型が決まる場面ならどこでも役に立ちます。
実践例:通知を送る
Section titled “実践例:通知を送る”もう少し本物のアプリに近い例を見てみましょう。プログラムはユーザーに通知を送る必要がありますが、ユーザーごとに選んだ方法が違います。メール、SMS、あるいはその両方です。通知の方法はそれぞれ別の型で、すべてが Notifier トレイトを共有しています。
trait Notifier { fn send(&self, message: &str) -> String;}
struct Email { address: String }struct Sms { phone: String }
impl Notifier for Email { fn send(&self, message: &str) -> String { format!("{}宛てにメール: {}", self.address, message) }}
impl Notifier for Sms { fn send(&self, message: &str) -> String { format!("{}宛てにSMS: {}", self.phone, message) }}
fn notify_all(channels: &[Box<dyn Notifier>], message: &str) { for channel in channels { println!("{}", channel.send(message)); }}
fn main() { let channels: Vec<Box<dyn Notifier>> = vec![ Box::new(Sms { phone: String::from("+1-555-0100") }), ];
notify_all(&channels, "ご注文の商品を発送しました!");}実行結果:
[email protected]宛てにメール: ご注文の商品を発送しました!+1-555-0100宛てにSMS: ご注文の商品を発送しました!うれしいのは、あとからプッシュ通知の Push を追加したくなっても、新しい構造体を1つと impl Notifier for Push を1つ書くだけで済むことです。notify_all 関数はまったく変更する必要がありません。
制限:dyn 互換性
Section titled “制限:dyn 互換性”すべてのトレイトがトレイトオブジェクトになれるわけではありません。トレイトは dyn 互換(dyn compatible)でなければなりません(古い本やブログ記事ではオブジェクト安全性、object safety と呼ばれています)。ルールをすべて挙げると長くなりますが、実際によく出会うのは次の2つです。
- メソッドが
Selfを返してはいけません。(dynの向こう側では Rust はSelfが何なのかを知らないので、戻り値の大きさもわかりません。) - メソッドが、
fn process<T>(&self, item: T)のような独自のジェネリックな型パラメータを持ってはいけません。
1つ目のルールを破ると、こうなります。
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self;}// ……あとで:let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })];error[E0038]: the trait `Shape` is not dyn compatible |14 | let shapes: Vec<Box<dyn Shape>> = vec![Box::new(Circle { radius: 1.0 })]; | ^^^^^ `Shape` is not dyn compatible | 1 | trait Shape { | ----- this trait is not dyn compatible... 2 | fn area(&self) -> f64; 3 | fn duplicate(&self) -> Self; | ^^^^ ...because method `duplicate` references the `Self` type in its return type = help: consider moving `duplicate` to another traitこのエラーは正直で親切です。「Shape トレイトは dyn 互換ではありません。メソッド duplicate の戻り値の型が Self を参照しているからです」と、問題のメソッドをはっきり教えてくれます。対処法は2つあります。そのメソッドを別のトレイトに移すか、メソッドに where Self: Sized を付けるかです。後者は Rust に「このメソッドは、トレイトオブジェクトではなく、正体がわかっている実際の型にだけ存在する」と伝えます。
trait Shape { fn area(&self) -> f64; fn duplicate(&self) -> Self where Self: Sized;}Clone のような標準のトレイトを dyn Clone として使えないのも、これが理由です。clone() は Self を返すからです。
よくある間違い
Section titled “よくある間違い”Box を忘れる。つい Vec<dyn Shape> と書きたくなります。
let shapes: Vec<dyn Shape> = Vec::new();error[E0277]: the size for values of type `dyn Shape` cannot be known at compilation time --> src/main.rs:3:17 |3 | let shapes: Vec<dyn Shape> = Vec::new(); | ^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = help: the trait `Sized` is not implemented for `dyn Shape`エラーは「dyn Shape 型の値のサイズはコンパイル時にわかりません」と言っています。dyn Shape の中身は、小さな Square かもしれないし、フィールドがたくさんある大きな構造体かもしれません。Rust は決まったサイズを必要とするので、トレイトオブジェクトはいつもポインタの向こう側に置く必要があります。Box<dyn Shape>、&dyn Shape、Rc<dyn Shape> のどれかです。
どこでも dyn を使う。関数が一度に1つの型しか受け取らないなら、impl Trait やジェネリクスのほうがシンプルで高速です(impl Trait について詳しくは高度なトレイトのページを見てください)。dyn は、本当に型を混ぜる必要があるときのために取っておきましょう。
1. 一つのVecに動物園
Section titled “1. 一つのVecに動物園”fn name(&self) -> String と fn sound(&self) -> String を持つトレイト Animal を定義してください。それを Dog、Cow、Duck に実装し、それぞれ一つずつ Vec<Box<dyn Animal>> に入れて、ループで各動物の鳴き声を表示します。ヒント:構造体は空っぽでかまいません(struct Dog;)。どれも Box::new(...) で入れます。
解答を見る
trait Animal { fn name(&self) -> String; fn sound(&self) -> String;}
struct Dog;struct Cow;struct Duck;
impl Animal for Dog { fn name(&self) -> String { String::from("犬") } fn sound(&self) -> String { String::from("ワン") }}
impl Animal for Cow { fn name(&self) -> String { String::from("牛") } fn sound(&self) -> String { String::from("モー") }}
impl Animal for Duck { fn name(&self) -> String { String::from("アヒル") } fn sound(&self) -> String { String::from("ガーガー") }}
fn main() { let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cow), Box::new(Duck)];
for animal in &zoo { println!("{}は「{}」と鳴きます", animal.name(), animal.sound()); }}実行結果:
犬は「ワン」と鳴きます牛は「モー」と鳴きますアヒルは「ガーガー」と鳴きます2. 実行時にあいさつ係を選ぶ
Section titled “2. 実行時にあいさつ係を選ぶ”fn hello(&self, name: &str) -> String を持つトレイト Greeter と、二つの型 English と Spanish を定義してください。"es" なら Spanish を、それ以外なら English を返す fn make_greeter(lang: &str) -> Box<dyn Greeter> を書きます。["en", "es", "fr"] のそれぞれのコードで "Mia" にあいさつしてください。(英語とスペイン語の違いを見る問題なので、あいさつの文字列はそのままです。)ヒント:if の両方の分岐は同じ型を返さなければならないので、どちらも Box::new(...) で包みます。
解答を見る
trait Greeter { fn hello(&self, name: &str) -> String;}
struct English;struct Spanish;
impl Greeter for English { fn hello(&self, name: &str) -> String { format!("Hello, {name}!") }}
impl Greeter for Spanish { fn hello(&self, name: &str) -> String { format!("¡Hola, {name}!") }}
fn make_greeter(lang: &str) -> Box<dyn Greeter> { if lang == "es" { Box::new(Spanish) } else { Box::new(English) }}
fn main() { for lang in ["en", "es", "fr"] { let greeter = make_greeter(lang); println!("{lang}: {}", greeter.hello("Mia")); }}実行結果:
en: Hello, Mia!es: ¡Hola, Mia!fr: Hello, Mia!Vecに入れられるのは1種類の型だけです。型を混ぜたいときは、Box<dyn Trait>のようなトレイトオブジェクトを使います。Box<dyn Trait>は値を所有し、&dyn Traitは値を借用します。トレイトオブジェクトはいつもポインタの向こう側に置かれます。- ジェネリクスと
impl Traitは静的ディスパッチ(高速で、コンパイル時に決まる)を使います。dyn Traitは動的ディスパッチ(実行時に vtable を参照する)を使います。 - 関数が実行時に違う型の中から返す型を選ぶときは、
Box<dyn Trait>を返します。 - トレイトオブジェクトになれるのは dyn 互換なトレイトだけです。
Selfを返すメソッドやジェネリックなメソッドがあってはいけません(where Self: Sizedが付いている場合を除く)。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. Circle と Square を同じふつうの Vec に入れられないのはなぜですか?
2. Box<dyn Shape> の dyn は Rust に何を伝えていますか?
3. 静的ディスパッチを使うのはどれですか?
4. Box のない Vec<dyn Shape> がエラーになるのはなぜですか?
5. トレイトを dyn 互換でなくしてしまうメソッドはどれですか?
スコア: 0 / 5
さらに詳しく
Section titled “さらに詳しく”- トレイトオブジェクトで異なる型の値を許容する — The Rust Programming Language 日本語版:公式の章です。
Box<dyn Draw>を使って、GUI風のコンポーネントのリストを作ります。 - dynを利用してトレイトを返す — Rust By Example 日本語版:
Box<dyn Trait>を返す関数の、短くて実行できる例。 - Trait objects — The Rust Reference(英語):ライフタイムや
Sendのような追加のマーカートレイトも含めた、dyn Trait型の正確なルール。 - Dyn compatibility — The Rust Reference(英語):トレイトを
dynで使うために守るべきルールの一覧。 dynキーワード — 標準ライブラリのドキュメント(英語):dynとvtable、そしてジェネリクスと比べたときのトレードオフを手短に説明しています。