[Rustのはじめ方] Part23: トレイトとトレイト境界

Rust

型の共通の振る舞いを定義し、ジェネリクスをより強力にする方法を学びましょう。

トレイトとは? 🤔

トレイトは、他のプログラミング言語におけるインターフェースに似た概念です。ある型が持つべき共通の振る舞い(メソッドのシグネチャ)を定義します。これにより、異なる型に対して同じメソッド呼び出しを行うことができ、コードの抽象化と再利用性が向上します。✨

トレイトは trait キーワードを使って定義します。中には、実装される型が持つべきメソッドのシグネチャを記述します。


trait Summary {
  fn summarize(&self) -> String; // summarize メソッドを持つことを要求
}
        

トレイトの実装 🛠️

定義したトレイトを特定の型に実装するには、impl トレイト名 for 型名 { ... } という構文を使います。ブロック内には、トレイトで定義されたメソッドの具体的な実装を記述します。

例えば、ニュース記事を表す Article 構造体と、ツイートを表す Tweet 構造体に Summary トレイトを実装してみましょう。


struct Article {
  headline: String,
  location: String,
  author: String,
  content: String,
}

impl Summary for Article {
  fn summarize(&self) -> String {
    format!("{}, by {} ({})", self.headline, self.author, self.location)
  }
}

struct Tweet {
  username: String,
  content: String,
  reply: bool,
  retweet: bool,
}

impl Summary for Tweet {
  fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}

// トレイトを実装した型のメソッドを呼び出す
let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
};

println!("1 new tweet: {}", tweet.summarize()); // 出力: 1 new tweet: horse_ebooks: of course, as you probably already know, people
        

このように、ArticleTweet は異なる型ですが、どちらも Summary トレイトを実装しているため、summarize メソッドを呼び出すことができます。

トレイト境界 (Trait Bounds) 🧱

ジェネリクスとトレイトを組み合わせることで、非常に強力な抽象化が可能になります。トレイト境界を使うと、ジェネリックな型パラメータが特定のトレイトを実装していることを要求できます。これにより、そのトレイトが提供するメソッドをジェネリックな関数内で安全に使用できるようになります。

トレイト境界は、型パラメータの後ろに : トレイト名 を付けて指定します。複数のトレイト境界は + で繋ぎます。where 句を使うと、複雑な境界をより読みやすく書くこともできます。


use std::fmt::Display; // 表示のためのトレイト

// ジェネリクス構文でのトレイト境界
// T は Summary トレイトを実装していなければならない
fn notify<T: Summary>(item: &T) {
  println!("Breaking news! {}", item.summarize());
}

// impl Trait 構文 (より簡潔な場合がある)
// item は Summary トレイトを実装する何らかの型
fn notify_impl(item: &impl Summary) {
  println!("Breaking news! {}", item.summarize());
}

// 複数のトレイト境界 (Summary と Display の両方を実装)
fn notify_and_display<T: Summary + Display>(item: &T) {
  println!("Breaking news! {}", item.summarize());
  println!("Display: {}", item); // Display トレイトの機能も使える
}

// where 句を使ったトレイト境界 (複雑な場合に可読性が向上)
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone, // T は Display と Clone を実装
    U: Clone + Summary, // U は Clone と Summary を実装
{
    // ... 関数の本体 ...
    0 // 仮の戻り値
}

fn main() {
    let article = Article {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from("The Pittsburgh Penguins are the best."),
    };

    notify(&article); // Article は Summary を実装しているので OK
    notify_impl(&article); // 同様に OK
}
        
impl Trait 構文は、引数の型や戻り値の型に対して、特定のトレイトを実装する「何らかの具体的な型」であることを示す簡単な方法です。ジェネリックな型パラメータを明示的に宣言するよりも簡潔に書ける場合があります。

デフォルト実装 📝

トレイト内のメソッドには、デフォルトの実装を提供することもできます。これにより、トレイトを実装する型は、必要に応じてそのメソッドをオーバーライドできますが、必須ではなくなります。

先ほどの Summary トレイトに、著者情報を取得する summarize_author メソッドを追加し、summarize メソッドにデフォルト実装を与えてみましょう。


trait Summary {
    fn summarize_author(&self) -> String; // このメソッドは各型で実装が必要

    // summarizeメソッドはデフォルト実装を持つ
    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author()) // summarize_authorを呼び出す
    }
}

// Tweet は summarize_author の実装のみ提供すれば良い
impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // summarize はデフォルト実装が使われる
}

// Article は summarize をオーバーライドすることも可能
impl Summary for Article {
    fn summarize_author(&self) -> String {
        format!("{}", self.author)
    }

    // summarize を独自実装でオーバーライド
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.summarize_author(), self.location)
    }
}

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("..."), // 省略
        reply: false,
        retweet: false,
    };
    let article = Article {
        headline: String::from("Penguins win..."), // 省略
        location: String::from("Pittsburgh"),
        author: String::from("Iceburgh"),
        content: String::from("..."),
    };

    println!("{}", tweet.summarize()); // 出力: (Read more from @horse_ebooks...)
    println!("{}", article.summarize()); // 出力: Penguins win..., by Iceburgh (Pittsburgh)
}
            

Tweetsummarize_author のみを実装し、summarize はデフォルト実装を利用しています。一方、Article は両方のメソッドを独自に実装(オーバーライド)しています。

戻り値としてのトレイト 🎁

関数の戻り値として、特定のトレイトを実装する型を返したい場合があります。ここでも impl Trait 構文が役立ちます。


fn returns_summarizable() -> impl Summary {
    // この関数は Summary トレイトを実装した *何か* を返す
    Tweet { // ここでは Tweet を返している
        username: String::from("impl_trait_user"),
        content: String::from("Returning impl Trait is useful!"),
        reply: false,
        retweet: false,
    }
    // 注意: この関数は *常に同じ具体的な型* (この場合はTweet) を返す必要があります。
    // 条件によって Article を返したり Tweet を返したりすることはできません。
}

fn main() {
    let summary_item = returns_summarizable();
    println!("Returned summary: {}", summary_item.summarize());
}
        

注意点 ⚠️

戻り値で impl Trait を使用する場合、関数は単一の具体的な型を返す必要があります。コンパイラは、関数が返す具体的な型を知っている必要がありますが、関数の呼び出し元にはその詳細を隠蔽します。例えば、ある条件では Tweet を返し、別の条件では Article を返すような関数は、この構文では直接書けません(その場合はトレイトオブジェクト(Box<dyn Trait>)など、別の手法が必要です)。

関連型 (Associated Types) 🧩

関連型は、トレイト定義内で使用されるプレースホルダー型です。これにより、トレイトを実装する際に、その型固有の具体的な型を指定できます。ジェネリクスだけでは表現しにくい関係性を定義するのに役立ちます。

標準ライブラリの Iterator トレイトが良い例です。イテレータが生成する値の型は、イテレータの種類によって異なります。この「値の型」を関連型 Item として定義しています。


pub trait Iterator {
    type Item; // Item が関連型

    fn next(&mut self) -> Option<Self::Item>; // nextメソッドは Option<関連型> を返す
    // ... 他のメソッド ...
}

// 例: u32 のイテレータを実装する場合
struct Counter { count: u32 }

impl Iterator for Counter {
    type Item = u32; // 関連型 Item を u32 に指定

    fn next(&mut self) -> Option<Self::Item> { // Self::Item は u32
        self.count += 1;
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}
            

関連型を使うことで、Iterator<T> のようにトレイト自体をジェネリックにする必要がなくなり、より簡潔で明確な定義が可能になります。

まとめ 🎉

トレイトとトレイト境界は、Rustの強力な型システムと抽象化能力の中核をなす機能です。

  • トレイトは、共通の振る舞いを定義するインターフェースです。
  • トレイト境界は、ジェネリックなコードが特定の振る舞いを持つ型でのみ動作するように制約します。
  • impl Trait は、引数や戻り値の型を簡潔に記述する方法を提供します。
  • デフォルト実装により、トレイト実装の手間を省き、柔軟性を高めます。
  • 関連型は、トレイト内で使用する具体的な型を、実装時に決定できるようにします。

これらの機能を理解し活用することで、より安全で、再利用性が高く、表現力豊かなRustコードを書くことができるようになります。💪

コメント

タイトルとURLをコピーしました