導入
第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がここでも活躍しています)。中身がCircleかRectangleかは、この時点では隠されています。Vec<Box<dyn Shape>>… 異なる具体的な型(CircleとRectangle)を、同じ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 ... で追加し、shapes の Vec に Box::new(...) で足してみましょう。型が違っても同じ Vec に入り、同じ for ループで処理できることを確かめてください。
演習
Shape を実装する新しい構造体 Triangle(フィールド base: f64 と height: f64、面積は 0.5 * base * height、name は "三角形")を定義してください。shapes に Circle { 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 Triangle の area は 0.5 * self.base * self.height、name は String::from("三角形") を返します。構造体定義・impl は Circle/Rectangle と同じ形です。
ヒント2を見る
let mut total = 0.0; for shape in &shapes { total += shape.area(); } println!("{:.2}", total); のように、for ループで合計を積み上げます。