トレイト: 共通の振る舞いを定義する

トレイトは、Rustコンパイラに、特定の型に存在し、他の型と共有できる機能について知らせます。 トレイトを使用すると、共通の振る舞いを抽象的に定義できます。トレイト境界を使用すると、 あるジェネリックが、特定の振る舞いをもつあらゆる型になり得ることを指定できます。

注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。

トレイトを定義する

型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全てに対して同じメソッドを呼び出せるなら、 同じ振る舞いを共有することになります。トレイト定義は、メソッドシグニチャをあるグループにまとめ、なんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。

例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所から送られる新しいニュースを保持するNewsArticleと、 新規ツイートか、リツイートか、はたまた他のツイートへのリプライなのかを示すメタデータを伴う最大で280文字までのTweetです。

NewsArticle または Tweet インスタンスに保存されているデータのサマリーを表示できるメディア アグリゲータ ライブラリを作成します。 これをするには、各型のサマリーが必要で、インスタンスで summarize メソッドを呼び出してサマリーを要求する必要があります。 リスト10-12は、この振る舞いを表現するSummaryトレイトの定義を表示しています。

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
}

リスト10-12: summarizeメソッドで提供される振る舞いからなるSummaryトレイト

ここでは、traitキーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、 Summaryです。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、 今回の場合は、fn summarize(&self) -> Stringです。

メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。 このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。 コンパイラにより、Summaryトレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarizeが定義されていることが 強制されます。

トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに並べられ、 各行はセミコロンで終わります。

トレイトを型に実装する

今や Summary トレイトを使用して目的の動作を定義できたので、メディア アグリゲータでこれを型に実装できます。 リスト10-13は、 Summary トレイトを NewsArticle 構造体上に実装したもので、ヘッドライン、著者、そして地域情報を使ってsummarize の戻り値を作っています。 Tweet 構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize を定義します。

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

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

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

リスト10-13: SummaryトレイトをNewsArticleTweet型に実装する

型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、implの後に、 実装したいトレイトの名前を置き、それからforキーワード、さらにトレイトの実装対象の型の名前を指定することです。 implブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、 波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。

トレイトを実装後、普通のメソッド同様にNewsArticleTweetのインスタンスに対してこのメソッドを呼び出せます。 こんな感じで:

use chapter10::{self, Summary, Tweet};

fn main() {
    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と出力します。

リスト10-13でSummaryトレイトとNewArticleTweet型を同じlib.rsに定義したので、 全部同じスコープにあることに注目してください。このlib.rsaggregatorと呼ばれるクレート専用にして、 誰か他の人が私たちのクレートの機能を活用して自分のライブラリのスコープに定義された構造体にSummaryトレイトを実装したいとしましょう。 まず、トレイトをスコープに取り込む必要があるでしょう。use aggregator::Summary;と指定してそれを行えば、 これにより、自分の型にSummaryを実装することが可能になるでしょう。Summaryトレイトは、 他のクレートが実装するためには、公開トレイトである必要があり、ここでは、リスト10-12のtraitの前に、 pubキーワードを置いたのでそうなっています。

トレイト実装で注意すべき制限の1つは、トレイトか対象の型が自分のクレートに固有(local)である時のみ、 型に対してトレイトを実装できるということです。例えば、Displayのような標準ライブラリのトレイトをaggregatorクレートの機能の一部として、 Tweetのような独自の型に実装できます。型Tweetaggregatorクレートに固有だからです。 また、SummaryaggregatorクレートでVec<T>に対して実装することもできます。 トレイトSummaryは、aggregatorクレートに固有だからです。

しかし、外部のトレイトを外部の型に対して実装することはできません。例として、 aggregatorクレート内でVec<T>に対してDisplayトレイトを実装することはできません。 DisplayVec<T>は標準ライブラリで定義され、aggregatorクレートに固有ではないからです。 この制限は、コヒーレンス(coherence)、特に孤児のルール(orphan rule)と呼ばれるプログラムの特性の一部で、 親の型が存在しないためにそう命名されました。この規則により、他の人のコードが自分のコードを壊したり、 その逆が起きないことを保証してくれます。この規則がなければ、2つのクレートが同じ型に対して同じトレイトを実装できてしまい、 コンパイラはどちらの実装を使うべきかわからなくなってしまうでしょう。

デフォルト実装

時として、全ての型の全メソッドに対して実装を要求するのではなく、トレイトの全てあるいは一部のメソッドに対してデフォルトの振る舞いがあると有用です。 そうすれば、特定の型にトレイトを実装する際、各メソッドのデフォルト実装を保持するかオーバーライドするか選べるわけです。

リスト10-14は、リスト10-12のように、メソッドシグニチャだけを定義するのではなく、 Summaryトレイトのsummarizeメソッドにデフォルトの文字列を指定する方法を示しています。

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize(&self) -> String {
        // "(もっと読む)"
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

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

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

リスト10-14: summarizeメソッドのデフォルト実装があるSummaryトレイトの定義

独自の実装を定義するのではなく、デフォルト実装を利用してNewsArticleのインスタンスをまとめるには、 impl Summary for NewsArticle {}と空のimplブロックを指定します。

もはやNewsArticleに直接summarizeメソッドを定義してはいませんが、私達はデフォルト実装を提供しており、 NewsArticleSummaryトレイトを実装すると指定しました。そのため、 NewsArticleのインスタンスに対してsummarizeメソッドを同じように呼び出すことができます。 このように:

use chapter10::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        // ペンギンチームがスタンレーカップチャンピオンシップを勝ち取る!
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        // アメリカ、ペンシルベニア州、ピッツバーグ
        location: String::from("Pittsburgh, PA, USA"),
        // アイスバーグ
        author: String::from("Iceburgh"),
        // ピッツバーグ・ペンギンが再度NHL(National Hockey League)で最強のホッケーチームになった
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

このコードは、New article available! (Read more...)新しい記事があります!(もっと読む))と出力します。

summarizeにデフォルト実装を用意しても、リスト10-13のTweetSummary実装を変える必要はありません。 理由は、デフォルト実装をオーバーライドする記法はデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。

デフォルト実装は、自らのトレイトのデフォルト実装を持たない他のメソッドを呼び出すことができます。 このようにすれば、トレイトは多くの有用な機能を提供しつつ、実装者は僅かな部分しか指定しなくて済むようになります。 例えば、Summaryトレイトを、(実装者が)内容を実装しなければならないsummarize_authorメソッドを持つように定義し、 それからsummarize_authorメソッドを呼び出すデフォルト実装を持つsummarizeメソッドを定義することもできます:


#![allow(unused)]
fn main() {
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        // "({}さんの文章をもっと読む)"
        format!("(Read more from {}...)", self.summarize_author())
    }
}

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

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

このバージョンのSummaryを使用するために、型にトレイトを実装する際、実装する必要があるのはsummarize_authorだけです:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        // "({}さんの文章をもっと読む)"
        format!("(Read more from {}...)", self.summarize_author())
    }
}

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

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

summarize_author定義後、Tweet構造体のインスタンスに対してsummarizeを呼び出せ、 summarizeのデフォルト実装は、私達が提供したsummarize_authorの定義を呼び出すでしょう。 summarize_authorを実装したので、追加のコードを書く必要なく、Summaryトレイトは、 summarizeメソッドの振る舞いを与えてくれました。

use chapter10::{self, Summary, Tweet};

fn main() {
    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: (Read more from @horse_ebooks...)1つの新しいツイート:(@horse_ebooksさんの文章をもっと読む))と出力します。

デフォルト実装を、そのメソッドをオーバーライドしている実装から呼び出すことはできないことに注意してください。

引数としてのトレイト

トレイトを定義し実装する方法はわかったので、トレイトを使っていろんな種類の型を受け付ける関数を定義する方法を学んでいきましょう。

たとえば、Listing 10-13では、NewsArticleTweet型にSummaryトレイトを実装しました。 ここで、引数のitemsummarizeメソッドを呼ぶ関数notifyを定義することができます。ただし、引数itemSummaryトレイトを実装しているような何らかの型であるとします。 このようなことをするためには、impl Trait構文を使うことができます。

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

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

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

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

引数のitemには、具体的な型の代わりに、implキーワードとトレイト名を指定します。 この引数は、指定されたトレイトを実装しているあらゆる型を受け付けます。 notifyの中身では、summarizeのような、Summaryトレイトに由来するitemのあらゆるメソッドを呼び出すことができます。 私達は、notifyを呼びだし、NewsArticleTweetのどんなインスタンスでも渡すことができます。 この関数を呼び出すときに、Stringi32のような他の型を渡すようなコードはコンパイルできません。 なぜなら、これらの型はSummaryを実装していないからです。

トレイト境界構文

impl Trait構文は単純なケースを解決しますが、実はより長いトレイト境界 (trait bound) と呼ばれる姿の糖衣構文 (syntax sugar) なのです。 それは以下のようなものです:

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

この「より長い」姿は前節の例と等価ですが、より冗長です。 山カッコの中にジェネリックな型引数の宣言を書き、型引数の後ろにコロンを挟んでトレイト境界を置いています。

簡単なケースに対し、impl Trait構文は便利で、コードを簡潔にしてくれます。 そうでないケースの場合、トレイト境界構文を使えば複雑な状態を表現できます。 たとえば、Summaryを実装する2つのパラメータを持つような関数を考えることができます。 impl Trait構文を使うとこのようになるでしょう:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

この関数が受け取るitem1item2の型が(どちらもSummaryを実装する限り)異なっても良いとするならば、impl Traitは適切でしょう。 両方の引数が同じ型であることを強制することは、以下のようにトレイト境界を使ってのみ表現可能です:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

引数であるitem1item2の型としてジェネリックな型Tを指定しました。 これにより、item1item2として関数に渡される値の具体的な型が同一でなければならない、という制約を与えています。

複数のトレイト境界を+構文で指定する

複数のトレイト境界も指定できます。 たとえば、notifysummarizeメソッドに加えてitemの画面出力形式(ディスプレイフォーマット)を使わせたいとします。 その場合は、notifyの定義にitemDisplaySummaryの両方を実装していなくてはならないと指定することになります。 これは、以下のように+構文で行うことができます:

pub fn notify(item: &(impl Summary + Display)) {

+構文はジェネリック型につけたトレイト境界に対しても使えます:

pub fn notify<T: Summary + Display>(item: &T) {

これら2つのトレイト境界が指定されていれば、notifyの中ではsummarizeを呼び出すことと、{}を使ってitemをフォーマットすることの両方が行なえます。

where句を使ったより明確なトレイト境界

あまりたくさんのトレイト境界を使うことには欠点もあります。 それぞれのジェネリック(な型)がそれぞれのトレイト境界をもつので、複数のジェネリック型の引数をもつ関数は、関数名と引数リストの間に大量のトレイト境界に関する情報を含むことがあります。 これでは関数のシグネチャが読みにくくなってしまいます。 このため、Rustはトレイト境界を関数シグネチャの後の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
{

この関数シグニチャは、よりさっぱりとしています。トレイト境界を多く持たない関数と同じように、関数名、引数リスト、戻り値の型が一緒になって近くにあるからですね。

トレイトを実装している型を返す

以下のように、impl Trait構文を戻り値型のところで使うことにより、あるトレイトを実装する何らかの型を返すことができます。

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

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

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

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

戻り値の型としてimpl Summaryを使うことにより、具体的な型が何かを言うことなく、returns_summarizable関数はSummaryトレイトを実装している何らかの型を返すのだ、と指定することができます。 今回returns_summarizableTweetを返しますが、この関数を呼び出すコードはそのことを知りません。

実装しているトレイトだけで戻り値型を指定できることは、13章で学ぶ、クロージャとイテレータを扱うときに特に便利です。 クロージャとイテレータの作り出す型は、コンパイラだけが知っているものであったり、指定するには長すぎるものであったりします。 impl Trait構文を使えば、非常に長い型を書くことなく、ある関数はIteratorトレイトを実装するある型を返すのだ、と簡潔に指定することができます。

ただし、impl Traitは一種類の型を返す場合にのみ使えます。 たとえば、以下のように、戻り値の型はimpl Summaryで指定しつつ、NewsArticleTweetを返すようなコードは失敗します:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

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

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

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

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            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 once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

NewsArticleTweetを返すというのは、コンパイラのimpl Trait構文の実装まわりの制約により許されていません。 このような振る舞いをする関数を書く方法は、17章のトレイトオブジェクトで異なる型の値を許容する節で学びます。

トレイト境界でlargest関数を修正する

ジェネリックな型引数の境界で使用したい振る舞いを指定する方法がわかったので、リスト10-5に戻って、 ジェネリックな型引数を使用するlargest関数の定義を修正しましょう!最後にそのコードを実行しようとした時、 こんなエラーが出ていました:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
  = note: `T` might need a bound for `std::cmp::PartialOrd`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`.

To learn more, run the command again with --verbose.

largestの本体で、大なり演算子(>)を使用して型Tの2つの値を比較しようとしていました。この演算子は、 標準ライブラリトレイトのstd::cmp::PartialOrdでデフォルトメソッドとして定義されているので、 largest関数が、比較できるあらゆる型のスライスに対して動くようにするためには、Tのトレイト境界にPartialOrdを指定する必要があります。 PartialOrdはpreludeに含まれているので、これをスコープに導入する必要はありません。 largestのシグニチャを以下のように変えてください:

fn largest<T: PartialOrd>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

今回のコンパイルでは、別のエラーが出てきます:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
(エラー[E0508]: 型`[T]`をもつ、非コピーのスライスからのムーブはできません)
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       (ここからムーブすることはできません)
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       (ムーブが発生するのは、`list[_]`は`T`という、`Copy`トレイトを実装しない型であるためです)
  |                       help: consider borrowing here: `&list[0]`
  |                       (助言:借用するようにしてみてはいかがですか: `&list[0]`)

error[E0507]: cannot move out of a shared reference
(エラー[E0507]:共有の参照からムーブはできません)
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |(データがここでムーブされています)
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         |(ムーブが発生するのは、`item`は`T`という、`Copy`トレイトを実装しない型であるためです)
  |         help: consider removing the `&`: `item`
  |         (助言:`&`を取り除いてみてはいかがですか: `item`)

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10`.

To learn more, run the command again with --verbose.

このエラーの鍵となる行は、cannot move out of type [T], a non-copy sliceです。 ジェネリックでないバージョンのlargest関数では、最大のi32charを探そうとするだけでした。 第4章のスタックのみのデータ: コピー節で議論したように、i32charのようなサイズが既知の型は スタックに格納できるので、Copyトレイトを実装しています。しかし、largest関数をジェネリックにすると、 list引数がCopyトレイトを実装しない型を含む可能性も出てきたのです。結果として、 list[0]から値をlargestにムーブできず、このエラーに陥ったのです。

このコードをCopyトレイトを実装する型だけを使って呼び出すようにしたいなら、Tのトレイト境界にCopyを追加すればよいです! リスト10-15は、関数に渡したスライスの値の型が、i32charなどのようにPartialOrdCopyを実装する限りコンパイルできる、ジェネリックなlargest関数の完全なコードを示しています。

ファイル名: src/main.rs

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

リスト10-15: PartialOrdCopyトレイトを実装するあらゆるジェネリックな型に対して動く、 largest関数の実際の定義

もしlargest関数をCopyを実装する型だけに制限したくなかったら、TCopyではなくCloneというトレイト境界を持つと指定することもできます。そうしたら、 largest関数に所有権が欲しい時にスライスの各値をクローンできます。clone関数を使用するということは、 Stringのようなヒープデータを持つ型の場合により多くのヒープ確保が発生する可能性があることを意味します。 そして、大量のデータを取り扱っていたら、ヒープ確保には時間がかかることもあります。

largestの別の実装方法は、関数がスライスのT値への参照を返すようにすることです。 戻り値の型をTではなく&Tに変え、それにより関数の本体を参照を返すように変更したら、 CloneCopyトレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。 これらの代替策をご自身で実装してみましょう!

トレイト境界を使用して、メソッド実装を条件分けする

ジェネリックな型引数を持つimplブロックにトレイト境界を与えることで、 特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、 リスト10-16の型Pair<T>は、常にnew関数を実装します。しかし、Pair<T>は、 内部の型Tが比較を可能にするPartialOrdトレイト出力を可能にするDisplayトレイトを実装している時のみ、 cmp_displayメソッドを実装します。

ファイル名: src/lib.rs


#![allow(unused)]
fn main() {
use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}
}

リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする

また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。 トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、 Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、 Displayトレイトを実装するあらゆる型にToStringトレイトを実装しています。 標準ライブラリのimplブロックは以下のような見た目です:

impl<T: Display> ToString for T {
    // --snip--
}

標準ライブラリにはこのブランケット実装があるので、Displayトレイトを実装する任意の型に対して、 ToStringトレイトで定義されたto_stringメソッドを呼び出せるのです。 例えば、整数はDisplayを実装するので、このように整数値を対応するString値に変換できます:


#![allow(unused)]
fn main() {
let s = 3.to_string();
}

ブランケット実装は、トレイトのドキュメンテーションの「実装したもの」節に出現します。

トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付き言語では、その型に定義されていないメソッドを呼び出せば、実行時 (runtime) にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようになる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時の振る舞いを確認するコードを書かなくても済みます。 そうすることで、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させます。

すでに使っている他のジェネリクスに、ライフタイムと呼ばれるものがあります。 ライフタイムは、型が欲しい振る舞いを保持していることではなく、必要な間だけ参照が有効であることを保証します。 ライフタイムがどうやってそれを行うかを見てみましょう。

関連キーワード:  実装, pub, Summary, self, summarize, 定義, impl, メソッド, largest, item