RustにおけるTraitとTrait境界のすべてを解剖する
Olivia Novak
Dev Intern · Leapcell

Rustのトレイトは、他のプログラミング言語でよく「インターフェース」と呼ばれるものと似ていますが、いくつかの違いがあります。トレイトは、特定の型が他の型と共有できる機能を持っていることをRustコンパイラに伝えます。トレイトを使用すると、共有される動作を抽象的な方法で定義できます。トレイト境界を使用すると、ジェネリック型が特定の動作を実装する必要があることを指定できます。
簡単に言うと、トレイトはRustのインターフェースのようなもので、型がそのトレイトを実装するときに提供する必要がある動作を定義します。トレイトは、複数の型間で共有される動作を制約することができ、ジェネリックプログラミングで使用する場合、ジェネリックをトレイトで指定された動作に準拠する型に制限できます。
トレイトの定義
異なる型が同じ動作を示す場合、トレイトを定義して、それらの型に実装できます。トレイトを定義するということは、動作を記述し、何らかの目的を達成するために必要な要件のセットをまとめることを意味します。
トレイトは、一連のメソッドを定義するインターフェースです。
pub trait Summary { // トレイト内のメソッドは宣言のみでよい fn summarize_author(&self) -> String; // このメソッドにはデフォルトの実装があり、他の型は自分で実装する必要はありません fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) } }
- これは、
Summary
という名前のトレイトを定義し、summarize_author
とsummarize
の2つのメソッドを提供します。 - トレイトのメソッドは宣言のみが必要です。その実装は特定の型に委ねられます。ただし、メソッドはデフォルトの実装を持つこともできます。この場合、
summarize
メソッドには、デフォルトの実装がないsummarize_author
を内部的に呼び出すデフォルトの実装があります。 Summary
トレイトの両方のメソッドは、構造体のメソッドと同様に、パラメータとしてself
を取ります。ここで、self
はトレイトメソッドへの最初の引数です。
注:実際には、
self
はself: Self
の省略形、&self
はself: &Self
の省略形、&mut self
はself: &mut Self
の省略形です。Self
はトレイトを実装する型を指します。たとえば、型Foo
がSummary
トレイトを実装する場合、実装内ではSelf
はFoo
を指します。
pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!("@{} posted a tweet...", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } pub struct Post { pub title: String, pub author: String, pub content: String, } impl Summary for Post { fn summarize_author(&self) -> String { format!("{} posted an article", self.author) } fn summarize(&self) -> String { format!("{} posted: {}", self.author, self.content) } } impl Summary for Tweet { fn summarize_author(&self) -> String { format!("{} tweeted", self.username) } fn summarize(&self) -> String { format!("@{} tweeted: {}", self.username, self.content) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; println!("{}", tweet.summarize()) }
トレイトとその実装が定義される場所に関する重要な原則があります。これは孤立規則と呼ばれます。型A
に対してトレイトT
を実装する場合、T
またはA
のいずれかが現在のクレートで定義されている必要があります。
この規則により、他の人が書いたコードが自分のコードを壊すことがなく、同様に、自分のコードが意図せずに他の人のコードを壊すことがなくなります。
関数パラメータとしてトレイトを使用する
トレイトは関数パラメータとして使用できます。トレイトをパラメータとして使用する関数を定義する例を次に示します。
pub fn notify(item: &impl Summary) { // trait parameter println!("Breaking news! {}", item.summarize()); }
パラメータitem
は、「Summary
トレイトを実装する値」を意味します。Summary
トレイトを実装する任意の型を、この関数の引数として使用できます。関数本体内では、トレイトで定義されたメソッドをパラメータで呼び出すこともできます。
トレイト境界
上記のimpl Trait
の使用は、実際にはシンタックスシュガーです。完全な構文はT: Summary
のようになり、これは_トレイト境界_と呼ばれます。
pub fn notify<T: Summary>(item: &T) { // trait bound println!("Breaking news! {}", item.summarize()); }
より複雑なユースケースでは、トレイト境界はより柔軟性と表現力を提供します。たとえば、Summary
トレイトを実装する2つのパラメータを取るような関数です。
pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // trait parameter pub fn notify<T: Summary>(item1: &T, item2: &T) {} // Generic T bound: requires item1 and item2 to be of the same type, and that T implements the Summary trait
+
を使用して複数のトレイト境界を指定する
単一の制約に加えて、パラメータが複数のトレイトを実装する必要があるなど、複数の制約を指定できます。
pub fn notify(item: &(impl Summary + Display)) {} // Sugar syntax pub fn notify<T: Summary + Display>(item: &T) {} // Full trait bound syntax
where
を使用したトレイト境界の簡素化
トレイト制約が多い場合、関数のシグネチャが読みにくくなることがあります。そのような場合は、where
句を使用して構文を整理できます。
// 複数のジェネリック型に多くのトレイト境界がある場合、シグネチャが読みにくくなる可能性があります fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... } // `where`を使用して簡素化し、関数名、パラメータ、戻り値の型を近づけます fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug { ... }
トレイト境界を使用したメソッドまたはトレイトの条件付き実装
トレイト境界をパラメータとして使用すると、特定の型とトレイトに基づいてメソッドを条件付きで実装でき、関数がさまざまな型の引数を受け入れることができるようになります。 例:
fn notify(summary: impl Summary) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<impl Summary>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Weibo { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
関数のsummary
パラメータは、具体的な型の代わりにimpl Summary
を使用します。 これは、関数がSummary
トレイトを実装する任意の型を受け入れることができることを意味します。
値を所有し、その具体的な型ではなく、特定のトレイトを実装していることのみを気にする場合は、Box
のようなスマートポインタをキーワードdyn
と組み合わせたトレイトオブジェクト形式を使用できます。
fn notify(summary: Box<dyn Summary>) { println!("notify: {}", summary.summarize()) } fn notify_all(summaries: Vec<Box<dyn Summary>>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets: Vec<Box<dyn Summary>> = vec![Box::new(tweet)]; notify_all(tweets); }
ジェネリクスでのトレイトの使用
ジェネリックプログラミングでジェネリック型を制約するためにトレイトがどのように使用されるかを見てみましょう。
以前の例で、notify
関数をfn notify(summary: impl Summary)
として定義した場合、具体的な型を指定するのではなく、summary
パラメータの型がSummary
トレイトを実装する必要があることを指定しました。実際、impl Summary
はジェネリックプログラミングにおけるトレイト境界のシンタックスシュガーです。impl Trait
を使用したコードは、次のように書き換えることができます。
fn notify<T: Summary>(summary: T) { println!("notify: {}", summary.summarize()) } fn notify_all<T: Summary>(summaries: Vec<T>) { for summary in summaries { println!("notify: {}", summary.summarize()) } } fn main() { let tweet = Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, }; let tweets = vec![tweet]; notify_all(tweets); }
関数からimpl Trait
を返す
impl Trait
を使用して、関数が特定のトレイトを実装する型を返すことを指定できます。
fn returns_summarizable() -> impl Summary { Tweet { username: String::from("haha"), content: String::from("the content"), reply: false, retweet: false, } }
impl Trait
を持つこの種のリターンタイプは、単一の具体的な型に解決される必要があります。関数が同じトレイトを実装する異なる型を返す可能性がある場合、これはコンパイルエラーになります。例えば:
fn returns_summarizable(switch: bool) -> impl Summary { if switch { Tweet { ... } // ここで2つの異なる型を返すことはできません } else { Post { ... } // ここで2つの異なる型を返すことはできません } }
上記のコードは、Tweet
とPost
の2つの異なる型を返すため、エラーが発生します。両方とも同じトレイトを実装している場合でも同様です。異なる型を返したい場合は、トレイトオブジェクトを使用する必要があります。
fn returns_summarizable(switch: bool) -> Box<dyn Summary> { if switch { Box::new(Tweet { ... }) // Trait object } else { Box::new(Post { ... }) // Trait object } }
まとめ
Rustのコアデザインゴールの1つはzero-cost abstractions(実行時のパフォーマンスを犠牲にすることなく、高レベルの言語機能を使用できるようにすること)です。このzero-cost abstractionの基礎は、ジェネリクスとトレイトです。これらにより、コンパイル中に高レベルの構文を効率的な低レベルのコードにコンパイルでき、ランタイム効率が向上します。
トレイトは共有される動作を抽象的な方法で定義し、トレイト境界は関数パラメータまたは戻り値の型に制約を定義します(例:impl SuperTrait
またはT: SuperTrait
)。トレイトとトレイト境界を使用すると、ジェネリック型パラメータを使用することで繰り返しを減らすことができます。また、これらのジェネリック型が実装する必要のある動作について、コンパイラに明確なガイダンスを提供できます。トレイト境界の情報をコンパイラに提供するため、コードで使用されている実際の型が正しい動作を提供するかどうかをコンパイラがチェックできます。
要約すると、Rustのトレイトは主に2つの目的を果たします。
- 動作の抽象化: インターフェースと同様に、共有機能の定義を通じて型の共通の動作を抽象化します。
- 型の制約: 型の動作を制約し、型が実装するトレイトに基づいて、型の範囲を絞り込みます。
私たちはLeapcellです。Rustプロジェクトをホストするための最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、およびRedisの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、または Rust で開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストや料金は発生しません。
比類のない費用対効果
- アイドル料金なしの従量課金制。
- 例:25ドルで平均応答時間60ミリ秒で694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察を得るためのリアルタイムのメトリックとロギング。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中できます。
ドキュメントで詳細をご覧ください!
Xでフォローしてください: @LeapcellHQ