👤

Rustのトレイトとトレイト境界を理解する

に公開

表紙

Trait は他のプログラミング言語で一般的に「インターフェース(interface)」と呼ばれる機能に似ていますが、いくつかの違いがあります。Trait は特定の型が他の型と共有できる機能を持つことを Rust コンパイラに伝えます。Trait を使用することで、共通の動作を抽象的な方法で定義できます。Trait Bound を使えば、ジェネリック型が特定の動作を持つ型であることを指定できます。

簡単に言えば、Trait は Rust におけるインターフェースであり、ある型がそのインターフェースを使用する際の動作を定義するものです。Trait を使えば、複数の型間で共通の動作を制約でき、ジェネリックプログラミングの際にはジェネリック型が Trait で定義された動作に従うよう制限することも可能です。

Trait の定義

異なる型が同じ動作を持つ場合、Trait を定義して、それらの型に対してその Trait を実装することができます。Trait の定義とは、いくつかのメソッドをまとめて、ある目的を達成するために必要な動作やインターフェースの集合を定義することです。

Trait は一連のメソッドを定義するインターフェースです:

pub trait Summary {
    // trait 内のメソッドは宣言だけでよい
    fn summarize_author(&self) -> String;
    // デフォルト実装が定義されているメソッドは、他の型で再実装する必要はない
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}
  • Summary という名前の Trait を定義しており、summarize_authorsummarize の 2 つのメソッドが提供する動作を含んでいます。
  • Trait 内のメソッドは宣言だけでよく、実装は具体的な構造体に任されます。ただし、メソッドにはデフォルト実装も定義可能で、上の summarize メソッドはその例です。このメソッドの内部では、デフォルト実装のない summarize_author メソッドを呼び出しています。
  • Summary Trait の 2 つのメソッドの引数には両方とも self キーワードが含まれており、構造体のメソッドと同様に、Trait メソッドの第 1 引数として self を使用します。

補足:実際には selfself: Self の省略形であり、&selfself: &Self&mut selfself: &mut Self の省略形です。Self は Trait を実装する現在の型を表しており、例えば Foo という型が Summary Trait を実装している場合、SelfFoo を指します。

pub trait Summary {

    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("@{}发表了帖子...", 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!("{}发表了贴子", self.author)
   }
   fn summarize(&self) -> String {
     format!("{}发表了贴子:{}", self.author, self.content)
   }
}
impl Summary for Tweet {
   fn summarize_author(&self) -> String {
     format!("{}发表了微博", self.username)
   }
   fn summarize(&self) -> String {
     format!("@{}发表了微博:{}", 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())
}

Trait の実装と定義の場所に関して、非常に重要な原則(オーファンルール)があります:型 A に対して Trait T を実装したい場合、A または T のどちらか一方は現在のスコープ内で定義されていなければなりません!

このルールは、他人の書いたコードが自分のコードを壊さないように、また自分のコードが意図せず他人のコードを壊すのを防ぐためのものです。

Trait を関数の引数として使用する

Trait は関数の引数として使用できます。以下に、Trait を引数として使用する関数の定義例を示します:

pub fn notify(item: &impl Summary) { // Trait を引数として指定
    println!("Breaking news! {}", item.summarize());
}

この関数の引数 item は、Summary Trait を実装している型であることを意味します。Summary を実装している任意の型をこの関数の引数として渡すことができ、関数内ではその Trait に定義されたメソッドを呼び出すことができます。

Trait Bound(トレイト境界)

上記で使用された impl Trait は実はシンタックスシュガー(構文糖)であり、完全な記述形式は次のようになります。T: Summary の形式を「トレイト境界(trait bound)」と呼びます。

pub fn notify<T: Summary> (item: &T) {
    println!("Breaking news! {}", item.summarize());
}

より複雑なケースでは、トレイト境界を使うことで、より柔軟かつ表現力のある構文を得ることができます。例えば、2 つの impl Summary を引数に取る関数の場合:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {} // impl Trait を使用
pub fn notify<T: Summary>(item1: &T, item2: &T) {}  // ジェネリック T を使った場合:item1 と item2 は同じ型で、T は Summary を実装している必要がある

+ を用いた複数トレイト境界の指定

単一のトレイトだけでなく、複数のトレイトを組み合わせて制約をかけることもできます。例えば、引数が SummaryDisplay の両方の Trait を実装していることを要求するには:

pub fn notify(item: &(impl Summary + Display)) {}  // シンタックスシュガー
pub fn notify<T: Summary + Display>(item: &T) {}   // 明示的なトレイト境界

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 {
      ....
}

Trait 境界で条件付きにメソッドや Trait を実装する

Trait 境界を使えば、特定の型かつ特定の Trait を満たす条件下でメソッドを実装することができます。これにより、関数は複数の異なる型を受け入れられるようになります。例えば:

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 Trait を実装しているあらゆる型の引数を受け入れることができます。

特定の Trait を実装しているかどうかだけを気にして、実際の具体的な型に依存しないようにしたい場合は、スマートポインタ Box とキーワード dyn を組み合わせた Trait オブジェクトを使用することができます:

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);
}

ジェネリックでの Trait の使用

最後に、ジェネリックプログラミングにおいて Trait を使用して、ジェネリック型の振る舞いを制約する方法を見てみましょう。

先ほどの notify 関数の例(fn notify(summary: impl Summary))では、summary パラメータの型として具体的な型ではなく、impl キーワードと Trait 名(Summary)を指定しています。実際、この impl Summary はジェネリックプログラミングにおける「トレイト境界」のシンタックスシュガーであり、以下のように書き換えることができます:

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 を使うことができます。これにより、関数が返す値が「特定の 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 { ... }  // 異なる型を返すことはできない
    } else {
        Post { ... }   // 異なる型を返すことはできない
    }
}

上記のコードでは、Tweet 型と Post 型という異なる型の値を返そうとしており、コンパイルエラーになります。

このような場合、異なる型を返したいのであれば、Trait オブジェクトを使用する必要があります:

fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
    if switch {
        Box::new(Tweet { ... }) // Trait オブジェクト
    } else {
        Box::new(Post { ... })  // Trait オブジェクト
    }
}

まとめ

Rust の設計目標のひとつとして「ゼロコスト抽象化」が非常に重要な要素となっています。これにより、Rust は高レベル言語としての表現力を持ちながら、実行時のパフォーマンスを犠牲にすることがありません。このゼロコスト抽象化を実現するための基盤となるのが、ジェネリクスと Trait です。Trait とジェネリクスは、コンパイル時に高レベルな構文を効率的な低レベルコードへと変換することで、実行時の効率を保ちます。

Trait は共通の振る舞いを抽象的に定義するものであり、Trait Bound は関数の引数や戻り値において型の制約を定義するものです。例えば impl SuperTraitT: SuperTrait のように書くことで、ジェネリック型に必要な振る舞いをコンパイラに明示することができます。これにより、使用する具体的な型が正しい動作を提供しているかどうかをコンパイラがチェックできるようになります。

まとめると、Trait の用途は以下のように整理できます:

  • 振る舞いの抽象化:インターフェースに似ており、型に共通する性質を抽出し、共通の動作を定義する
  • 型の制約:Trait によって型の振る舞いを限定することで、より厳密な設計が可能となる

私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion