👪

RustにおけるTraitの解説:基本から応用まで

2025/03/02に公開

表紙

trait とは

Rust において、trait は共有の振る舞いを定義する方法です。trait を使うことで、特定の型が実装しなければならないメソッドを指定でき、ポリモーフィズムやインターフェースの抽象化を実現できます。

以下に、Printable という名前の trait を定義する簡単な例を示します。この trait には print というメソッドが含まれています。

trait Printable {
    fn print(&self);
}

trait の定義と実装

trait を定義するには、trait キーワードを使用し、その後に trait 名と中括弧 {} を続けます。中括弧内に、この trait に含めるメソッドを定義します。

trait を実装するには、impl キーワードを使用し、続けて trait 名と for キーワードを記述し、その後に trait を実装する型を指定します。中括弧 {} の中で、その型に対して trait に含まれるすべてのメソッドを実装する必要があります。

以下の例では、先ほど定義した Printable trait を i32 型に対して実装しています。

impl Printable for i32 {
    fn print(&self) {
        println!("{}", self);
    }
}

この例では、i32 型に対して Printable trait を実装し、print メソッドのシンプルな実装を提供しています。

trait の継承と合成

Rust では、既存の trait を拡張するために、継承や合成を使用できます。継承を使うと、親 trait に定義されたメソッドを新しい trait で再利用できます。一方、合成を使うと、複数の異なる trait を新しい trait に組み合わせることができます。

以下は、継承を使って Printable trait を拡張する方法を示した例です。

trait PrintableWithLabel: Printable {
    fn print_with_label(&self, label: &str) {
        print!("{}: ", label);
        self.print();
    }
}

この例では、新しい PrintableWithLabel trait を定義し、Printable trait を継承しています。つまり、PrintableWithLabel を実装する型は、必ず Printable trait も実装しなければなりません。さらに、PrintableWithLabel には print_with_label という新しいメソッドが定義されており、値を出力する前にラベルを表示できます。

次に、合成を使用して新しい trait を定義する例を示します。

trait DisplayAndDebug: Display + Debug {}

この例では、新しい DisplayAndDebug trait を定義し、標準ライブラリの DisplayDebug trait を組み合わせています。したがって、DisplayAndDebug trait を実装する型は、DisplayDebug の両方を実装している必要があります。

trait をパラメータや戻り値として使用する

Rust では、関数のシグネチャ内で trait をパラメータや戻り値として使用できます。これにより、より汎用的で柔軟なコードを記述できます。

以下に、先ほど定義した PrintableWithLabel trait を関数のパラメータとして使用する例を示します。

fn print_twice<T: PrintableWithLabel>(value: T) {
    value.print_with_label("First");
    value.print_with_label("Second");
}

この例では、print_twice という関数を定義し、汎用型 T を引数として受け取ります。この TPrintableWithLabel trait を実装している必要があります。そして、関数内では valueprint_with_label メソッドを 2 回呼び出しています。

次に、trait を関数の戻り値として使用する例を示します。

fn get_printable() -> impl Printable {
    42
}

しかし、fn get_printable() -> impl Printable { 42 } というコードは正しくありません。なぜなら 42 は整数型であり、Printable trait を実装していないためです。

正しい方法としては、Printable trait を実装した型を戻り値として返すことです。例えば、i32 型に対して Printable trait を実装すれば、次のように記述できます。

impl Printable for i32 {
    fn print(&self) {
        println!("{}", self);
    }
}

fn get_printable() -> impl Printable {
    42
}

この例では、i32 型に Printable trait を実装し、シンプルな print メソッドを定義しています。そして、get_printable 関数は i32 型の値 42 を返します。i32Printable trait を実装しているため、このコードは正しく動作します。

trait オブジェクトと静的ディスパッチ

Rust では、ポリモーフィズムを実現するために、静的ディスパッチと動的ディスパッチの 2 つの方法を使用できます。

静的ディスパッチ

静的ディスパッチは、ジェネリクスを使用することで実現されます。ジェネリクスを使うと、コンパイラはそれぞれの型ごとに個別のコードを生成します。これにより、どのメソッドを呼び出すかがコンパイル時に決定されます。

動的ディスパッチ

一方、動的ディスパッチは trait オブジェクトを使用することで実現されます。trait オブジェクトを使用すると、コンパイラは汎用的なコードを生成し、どのメソッドを呼び出すかは実行時に決定されます。

以下に、静的ディスパッチと動的ディスパッチを使用してポリモーフィズムを実現する例を示します。

fn print_static<T: Printable>(value: T) {
    value.print();
}

fn print_dynamic(value: &dyn Printable) {
    value.print();
}

この例では、2 つの関数 print_staticprint_dynamic を定義しています。

  • print_static 関数はジェネリクス T を使用しており、TPrintable trait を実装している必要があります。この関数を呼び出すと、コンパイラはそれぞれの型に対して個別のコードを生成します(静的ディスパッチ)。
  • print_dynamic 関数は &dyn Printable という trait オブジェクトを受け取ります。この場合、コンパイラは 1 つの汎用的なコードを生成し、実行時にどのメソッドを呼び出すかを決定します(動的ディスパッチ)。

関連型とジェネリック制約

Rust では、より複雑な trait を定義するために、関連型やジェネリック制約を使用できます。

関連型(Associated Types)

関連型を使用すると、trait 内で他の型と関連付けられる型を定義できます。これにより、trait 内で特定の型に依存するメソッドを定義することが可能になります。

以下に、関連型を使用して Add という trait を定義する例を示します。

trait Add<RHS = Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

この例では、Add trait を定義し、次のような要素を含んでいます。

  • Output という関連型を定義
  • add というメソッドを定義し、RHS というジェネリック型の引数を受け取る(デフォルトは Self
  • add メソッドは Self::Output 型の値を返す

このように、関連型を使用すると、より柔軟な trait の設計が可能になります。

ジェネリック制約(Generic Bounds)

ジェネリック制約を使用すると、ジェネリック型が満たすべき条件を指定できます。例えば、「あるジェネリック型は特定の trait を実装していなければならない」といった制約を加えることができます。

以下に、ジェネリック制約を使用して SummableIterator という trait を定義する例を示します。

use std::iter::Sum;

trait SummableIterator: Iterator
where
    Self::Item: Sum,
{
    fn sum(self) -> Self::Item {
        self.fold(Self::Item::zero(), |acc, x| acc + x)
    }
}

この例では、以下のように SummableIterator trait を定義しています。

  • Iterator trait を継承
  • Self::ItemSum trait を実装していることを要求(ジェネリック制約)
  • sum メソッドを定義し、イテレータのすべての要素を合計する

ジェネリック制約を使うことで、型の安全性を維持しながら、より柔軟な trait の設計が可能になります。

実例: trait を使用したポリモーフィズムの実装

以下の例では、先ほど定義した PrintableWithLabel trait を使用してポリモーフィズムを実装する方法を示します。

struct Circle {
    radius: f64,
}

impl Printable for Circle {
    fn print(&self) {
        println!("Circle with radius {}", self.radius);
    }
}

impl PrintableWithLabel for Circle {}

struct Square {
    side: f64,
}

impl Printable for Square {
    fn print(&self) {
        println!("Square with side {}", self.side);
    }
}

impl PrintableWithLabel for Square {}

fn main() {
    let shapes: Vec<Box<dyn PrintableWithLabel>> = vec![
        Box::new(Circle { radius: 1.0 }),
        Box::new(Square { side: 2.0 }),
    ];

    for shape in shapes {
        shape.print_with_label("Shape");
    }
}

この例のポイント

  1. CircleSquare という 2 つの構造体を定義
  2. PrintablePrintableWithLabel trait をそれぞれの構造体に実装
  3. main 関数で Vec<Box<dyn PrintableWithLabel>> を作成し、異なる型のオブジェクトを格納
  4. print_with_label メソッドを呼び出し、ラベル付きで各図形を出力

このコードでは、CircleSquare の 2 種類の図形を PrintableWithLabel trait オブジェクトとして Vec に格納し、それらをループ処理して print_with_label を呼び出しています。これにより、異なる型のオブジェクトを一元的に管理し、ポリモーフィズムを実現しています。

本記事では、Rust における trait の基本概念と応用について解説しました。trait は Rust における重要な概念であり、ポリモーフィズムやコードの再利用を促進する強力なツールです。ぜひ実際にコードを書きながら、その便利さを体験してみてください。


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

Leapcell

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

複数言語サポート

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

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

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

比類のないコスト効率

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

洗練された開発者体験

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

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

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

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

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion