コンテンツにスキップ

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 を使いましょう。速度の差はあまり気にしなくて大丈夫です。ほとんどのプログラムでは、違いに気づくことすらありません。

関数が、どの型を返すかを実行時に決めたいこともあります。-> 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の正方形を作りました
▶ 実際に試してみよう

これは「ファクトリ」関数やプラグインの仕組み、設定ファイルで動きが変わるコードなどでよく使うパターンです。プログラムを実行してみるまでわからないデータによって、正確な型が決まる場面ならどこでも役に立ちます。

もう少し本物のアプリに近い例を見てみましょう。プログラムはユーザーに通知を送る必要がありますが、ユーザーごとに選んだ方法が違います。メール、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(Email { address: String::from("[email protected]") }),
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 互換(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 を返すからです。

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 は、本当に型を混ぜる必要があるときのために取っておきましょう。

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());
}
}

実行結果:

犬は「ワン」と鳴きます
牛は「モー」と鳴きます
アヒルは「ガーガー」と鳴きます
▶ 実際に試してみよう

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 が付いている場合を除く)。

理解度チェック

1. Circle と Square を同じふつうの Vec に入れられないのはなぜですか?

2. Box<dyn Shape> の dyn は Rust に何を伝えていますか?

3. 静的ディスパッチを使うのはどれですか?

4. Box のない Vec<dyn Shape> がエラーになるのはなぜですか?

5. トレイトを dyn 互換でなくしてしまうメソッドはどれですか?

スコア: 0 / 5