トレイト境界の構文
CoddyのRustジャーニー「Object Oriented Programming」セクションの一部 — レッスン 40/61。
これまで、トレイトを定義し、それを構造体(structs)に対して実装する方法を学んできました。また、柔軟なコードを書くためにジェネリクスも扱ってきました。今度はこれらの概念を組み合わせる番です。トレイトを使用して、ジェネリック関数が受け入れることができる型を制約します。
引数に対して特定のメソッドを呼び出す必要があるジェネリック関数を考えてみましょう。何の制約もなければ、Rustは型Tがそのメソッドを持っていることを保証できません。
fn print_info<T>(item: T) {
println!("{}", item.summarize()); // エラー!Tはsummarize()を持っていない可能性があります
}
解決策はトレイト境界 (trait bound)です。ジェネリックパラメータの後に: TraitNameを追加することで、Tはそのトレイトを実装していなければならないことをRustに伝えます。
trait Summary {
fn summarize(&self) -> String;
}
fn print_info<T: Summary>(item: T) {
println!("{}", item.summarize()); // これでRustはこのメソッドが存在することを知ることができます
}
構文<T: Summary>は、「TはSummaryを実装する任意の型である」と読みます。これにより、この関数は必要な振る舞いを持つ型のみを受け付けるようになります。もしSummaryを実装していない型を渡そうとすると、コンパイラは明確なエラーを出して拒否します。
このパターンは、特定の機能にアクセスしながらも汎用的なコードを記述できるため、非常に強力です。関数は柔軟性を保ち、あらゆる型で動作しますが、必要な振る舞いを提供する型のみに限定されます。
チャレンジ
簡単適切にフォーマットされたメッセージのみを送信できるように、トレイト境界(trait bounds)を使用した通知システムを構築しましょう!特定のトレイトを実装する任意の型を受け入れるジェネリック関数を作成し、トレイト境界がどのようにジェネリックパラメータを制約するかを学びます。
コードは2つのファイルに分けて構成します:
notification.rs:&selfを取りStringを返すmessageというメソッドを持つ、パブリックなNotifyトレイトを定義します。次に、このトレイトを実装する2つのパブリックな構造体を作成します:Email— パブリックなsubjectフィールド(String)を持ちます。そのmessageメソッドは"Email: {subject}"を返す必要があります。SMS— パブリックなcontentフィールド(String)を持ちます。そのmessageメソッドは"SMS: {content}"を返す必要があります。
Notifyトレイトを実装する任意の型Tを受け入れる、send_notificationというパブリックなジェネリック関数を作成します。この関数は、そのアイテムに対してmessage()を呼び出した結果をプリントする必要があります。main.rs: 通知モジュールを取り込み、提供された入力を使用してEmailとSMSの両方のインスタンスを作成します。それぞれのインスタンスでsend_notificationを呼び出し、ジェネリック関数がNotifyを実装している任意の型で動作することを確認してください。
ここでの重要な洞察は、send_notification は受け取る具体的な型を知らないということです。型がメッセージを生成できることだけを知っています。トレイト境界 T: Notify がこの能力を保証します。
出力には、両方の通知が送信されたことが表示されるはずです:
Email: {subject}
SMS: {content}例えば、入力が Meeting Tomorrow と On my way! の場合:
Email: Meeting Tomorrow
SMS: On my way!入力として、メールの件名(subject)とSMSの内容(content)の2つを受け取ります。
チートシート
トレイト境界は、特定のトレイトを実装する型のみを受け入れるようにジェネリック型パラメータを制約します。
トレイト境界の構文は、ジェネリックパラメータの後に : TraitName を使用します:
fn function_name<T: TraitName>(item: T) {
// itemに対してTraitNameのメソッドを呼び出せるようになります
}
トレイト境界を使用した例:
trait Summary {
fn summarize(&self) -> String;
}
fn print_info<T: Summary>(item: T) {
println!("{}", item.summarize());
}
<T: Summary> という表記は、「TはSummaryを実装する任意の型である」ことを意味します。これにより、関数はジェネリックな性質を維持したまま、トレイトのメソッドを呼び出すことができます。
自分で試してみよう
mod notification;
use notification::{Email, SMS, send_notification};
fn main() {
// 入力を読み込む
let mut subject = String::new();
std::io::stdin().read_line(&mut subject).expect("Failed to read line");
let subject = subject.trim().to_string();
let mut content = String::new();
std::io::stdin().read_line(&mut content).expect("Failed to read line");
let content = content.trim().to_string();
// TODO: 件名を使用してEmailインスタンスを作成する
// TODO: 内容を使用してSMSインスタンスを作成する
// TODO: emailを使用してsend_notificationを呼び出す
// TODO: smsを使用してsend_notificationを呼び出す
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。