本文へスキップ
BecomeCoder

Rustコース · 第11章 Rustらしい書き方 ― クロージャとイテレータ · レッスン45

トレイトオブジェクト ― dyn とジェネリック境界

ブラウザで完結

導入

第9章では fn describe<T: Shape>(s: &T) のようなジェネリクスとトレイト境界を学びました。これは「呼び出すたびに型が1つに決まる」場面では便利ですが、**「異なる型を1つのリストにまとめて入れたい」**場面では使えません。Vec<T>T は1つの型にしかなれないからです。この問題を解決するのが トレイトオブジェクトdyn です。

説明

トレイトを定義し、複数の構造体に実装するところまでは第9章と同じです。

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14159 * self.radius * self.radius
    }
    fn name(&self) -> String {
        String::from("円")
    }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
    fn name(&self) -> String {
        String::from("長方形")
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 2.0 }),
        Box::new(Rectangle { width: 3.0, height: 4.0 }),
    ];

    for shape in &shapes {
        println!("{}: {:.2}", shape.name(), shape.area());
    }
}
  • Box<dyn Shape>Box::new(...) でヒープに置いた値を、「Shape を実装している何か」という共通の型として扱います(第10章の Box がここでも活躍しています)。中身が CircleRectangle かは、この時点では隠されています。
  • Vec<Box<dyn Shape>> … 異なる具体的な型(CircleRectangle)を、同じ Vec に入れられます。
  • shape.area() … どの型の area を呼ぶかは、実行時に中身を見て決まります。これを動的ディスパッチと呼びます。

これを、第9章のジェネリック境界(静的ディスパッチ)と対比してみましょう。

fn describe<T: Shape>(s: &T) {
    println!("{}: {:.2}", s.name(), s.area());
}

fn main() {
    let c = Circle { radius: 2.0 };
    describe(&c);
}

describe<T: Shape> は、呼ばれるたびに T が1つの具体的な型に決まり、コンパイル時に型ごとの専用コードが生成されます(第9章で見た単相化)。速いですが、describe の1回の呼び出しでは1つの型しか扱えません。一方 Box<dyn Shape> は、実行時にどの実装を呼ぶか調べる分わずかにコストがありますが、型の違う値を同じ入れ物にまとめられるという表現力を手に入れます。

graph LR
    subgraph static["静的ディスパッチ(ジェネリクス)"]
        s1["describe(&c)"] --> s2["コンパイル時に<br/>Circle::area を直接呼ぶコードを生成"]
    end
    subgraph dynamic["動的ディスパッチ(dyn)"]
        d1["shape.area()"] --> d2["実行時に中身の型を見て<br/>対応する area を呼ぶ"]
    end

「型が1つに決まっているならジェネリクス、異なる型を1つのコレクションにまとめたいなら dyn」——この使い分けが、実務で dyn を選ぶときの判断基準です。

やってみよう

下のエディタで新しい図形を impl Shape for ... で追加し、shapesVecBox::new(...) で足してみましょう。型が違っても同じ Vec に入り、同じ for ループで処理できることを確かめてください。

演習

Shape を実装する新しい構造体 Triangle(フィールド base: f64height: f64、面積は 0.5 * base * heightname"三角形")を定義してください。shapesCircle { radius: 2.0 }Rectangle { width: 3.0, height: 4.0 }Triangle { base: 6.0, height: 4.0 } の3つを Box::new で詰め、for で回しながら面積の合計を計算して、{:.2}println! してください。

ヒント1を見る

impl Shape for Trianglearea0.5 * self.base * self.heightnameString::from("三角形") を返します。構造体定義・impl は Circle/Rectangle と同じ形です。

ヒント2を見る

let mut total = 0.0; for shape in &shapes { total += shape.area(); } println!("{:.2}", total); のように、for ループで合計を積み上げます。

実際に動かしてみよう

下のエディタにRustを書いて「実行」を押すと、学習用シミュレータが println! の出力を表示します(本物のrustcではなく、教材の範囲を再現した軽量エンジンです)。struct・enum・match・トレイト・Vec/String/HashMap・Option/Result なども動きます。本文の例を書き換えて動かしてみましょう(所有権・借用チェッカーのコンパイルエラーは再現しないため、動きの確認用として使ってください)。

Rust — ブラウザ内で実行(学習用シミュレータ)

Rustの教材サブセットを動かす学習用シミュレータを読み込みます(本物のrustcではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。