トレイトのベクタ
CoddyのRustジャーニー「Object Oriented Programming」セクションの一部 — レッスン 46/61。
Box<dyn Trait> によって、単一の変数が異なる型を保持できることを見てきました。これをコレクションと組み合わせることで、真の力が発揮されます。つまり、単一のベクタ内に複数の異なる型を格納することです。
通常のベクタでは、すべての要素が同じ型である必要があります。猫も含まれている Vec<Dog> を持つことはできません。しかし、トレイトオブジェクトを使用すると、共通のトレイトを実装しているあらゆるものを保持するベクタを作成できます:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
struct Bird;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
impl Speak for Bird {
fn speak(&self) -> String { String::from("Tweet!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Bird),
];
}
Vec<Box<dyn Speak>> という型は、「ボックス化されたトレイトオブジェクトのベクタ」を意味します。各要素は Speak を実装する何かを指す Box であり、実際の型は異なる場合があります。ベクタに追加する際、各値を Box::new() でラップします。
このパターンは、UIコンポーネント、ゲームエンティティ、プラグインシステムのように、関連性はあるが異なるオブジェクトのコレクションを管理する必要がある、柔軟なシステムを構築するために不可欠です。
チャレンジ
簡単異なる種類のアラートを処理できる通知システムを構築しましょう!トレイトオブジェクトを使用して、メール、SMSメッセージ、プッシュ通知といった様々な通知タイプを単一のベクタに格納するコレクションを作成します。
コードは2つのファイルに分けて構成します:
notifications.rs:&selfを受け取り、送信される通知を説明するStringを返すsendメソッドを持つ、パブリックなNotifyトレイトを定義します。次に、3つのパブリックな構造体を作成します:Email— パブリックなrecipientフィールド (String) を持ちます。そのsendは"Email to: {}"の形式でrecipientを含めて返す必要があります。Sms— パブリックなphoneフィールド (String) を持ちます。そのsendは"SMS to: {}"の形式でphoneを含めて返す必要があります。Push— パブリックなdeviceフィールド (String) を持ちます。そのsendは"Push to: {}"の形式でdeviceを含めて返す必要があります。
main.rs: notificationsモジュールを取り込み、3つの通知タイプすべてを保持するVec<Box<dyn Notify>>型のベクタを作成します。提供された入力を使用して各通知タイプを1つずつ作成し、それらを順番(Email、Sms、Push)にベクタに追加します。その後、ベクタの最初の要素に対してsend()を呼び出した結果をプリントしてください。
ここでの重要な概念は、Email、Sms、Push が完全に異なる構造体であるにもかかわらず、Notify トレイトを共有しているため、同じベクタ内に共存できるということです。各要素は Box::new() でラップされ、トレイトオブジェクトとして作成されます。
出力には最初の通知が表示されるはずです:
Email to: {recipient}例えば、入力が "alice@example.com"、"555-1234"、"iPhone-12" の場合:
Email to: alice@example.comメールの受信者、電話番号、デバイス名の3つの入力を受け取ります。
チートシート
Vec<Box<dyn Trait>> を使用すると、すべての型が同じトレイトを実装している限り、単一のベクタ内に複数の異なる型を格納できます。
通常のベクタではすべての要素が同じ型である必要がありますが、トレイトオブジェクトを使用すると、異なる型が混在するコレクション(ヘテロジニアスなコレクション)が可能になります:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
];
}
ベクタに追加する際、各要素は Box::new() でラップされます。共通のトレイトを実装している限り、実際の型は異なっていても構いません。
自分で試してみよう
mod notifications;
use notifications::{Notify, Email, Sms, Push};
fn main() {
// 入力を読み込む
let mut recipient = String::new();
std::io::stdin().read_line(&mut recipient).expect("Failed to read line");
let recipient = recipient.trim().to_string();
let mut phone = String::new();
std::io::stdin().read_line(&mut phone).expect("Failed to read line");
let phone = phone.trim().to_string();
let mut device = String::new();
std::io::stdin().read_line(&mut device).expect("Failed to read line");
let device = device.trim().to_string();
// TODO: 異なる通知タイプを保持するために Vec<Box<dyn Notify>> を作成する
// TODO: 入力を使用して Email、Sms、Push のインスタンスを作成する
// TODO: Box::new() を使用して、それらを順番(Email、Sms、Push)にベクタに追加する
// TODO: 最初の要素に対して send() を呼び出した結果を出力する
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。