トレイト:共有動作の定義

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

トレイト: 共有振る舞いの定義へようこそ。この実験は、Rust ブックの一部です。LabEx で Rust のスキルを練習することができます。

この実験では、型における共有振る舞いを定義し、ジェネリック型に対するトレイト境界を指定するための方法としてトレイトを探ります。

トレイト: 共有振る舞いの定義

「トレイト」は、特定の型が持ち、他の型と共有できる機能を定義します。トレイトを使って、共有振る舞いを抽象的な方法で定義することができます。「トレイト境界」を使って、ジェネリック型が特定の振る舞いを持つ任意の型であることを指定することができます。

注: トレイトは、他の言語でよく「インターフェイス」と呼ばれる機能に似ていますが、いくつかの違いがあります。

トレイトの定義

型の振る舞いは、その型で呼び出せるメソッドで構成されます。すべての型で同じメソッドを呼び出せる場合、異なる型は同じ振る舞いを共有します。トレイトの定義は、メソッドのシグネチャをまとめて、ある目的を達成するために必要な一連の振る舞いを定義する方法です。

たとえば、さまざまな種類と量のテキストを保持する複数の構造体があるとしましょう。特定の場所に保存されたニュース記事を保持する NewsArticle 構造体と、最大280文字のツイートと、それが新しいツイート、リツイート、または他のツイートへの返信であることを示すメタデータを持つ Tweet です。

私たちは、NewsArticle または Tweet インスタンスに格納されるデータの要約を表示できるメディアアグリゲータライブラリクレートである aggregator を作成したいと思います。これを行うには、各型から要約を取得する必要があり、インスタンスの summarize メソッドを呼び出してその要約を要求します。リスト10-12は、この振る舞いを表現するパブリックな Summary トレイトの定義を示しています。

ファイル名: src/lib.rs

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

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

ここでは、trait キーワードを使ってトレイトを宣言し、その後にトレイトの名前を指定します。この場合、その名前は Summary です。また、このトレイトを pub として宣言しているため、このクレートに依存するクレートもこのトレイトを利用できます。これについては、いくつかの例で見ていきます。波括弧の中では、このトレイトを実装する型の振る舞いを記述するメソッドのシグネチャを宣言します。この場合、それは fn summarize(&self) -> String です。

メソッドのシグネチャの後に、波括弧の中に実装を記述する代わりにセミコロンを使っています。このトレイトを実装する各型は、メソッドの本体に独自のカスタム振る舞いを提供する必要があります。コンパイラは、Summary トレイトを持つ任意の型がこのシグネチャで正確に定義された summarize メソッドを持つことを強制します。

トレイトの本体には複数のメソッドがあり得ます。メソッドのシグネチャは1行に1つずつリストされ、各行はセミコロンで終わります。

型にトレイトを実装する

これで、Summary トレイトのメソッドの望ましいシグネチャを定義したので、メディアアグリゲータの型に対してそれを実装することができます。リスト10-13は、見出し、著者、場所を使って summarize の返却値を作成する NewsArticle 構造体に対する Summary トレイトの実装を示しています。Tweet 構造体については、ツイートの内容が既に280文字に制限されていると仮定して、summarize をユーザー名に続けてツイートの全文として定義します。

ファイル名: src/lib.rs

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: NewsArticle 型と Tweet 型に対する Summary トレイトの実装

型にトレイトを実装することは、通常のメソッドを実装することと似ています。違いは、impl の後に、実装したいトレイト名を書き、その後に for キーワードを使って、トレイトを実装したい型の名前を指定することです。impl ブロックの中には、トレイト定義で定義されたメソッドのシグネチャを書きます。各シグネチャの後にセミコロンを追加する代わりに、波括弧を使って、トレイトのメソッドが特定の型に対して持つべき特定の振る舞いでメソッド本体を埋めます。

ライブラリが NewsArticleTweet に対して Summary トレイトを実装したので、クレートのユーザーは、通常のメソッドを呼ぶのと同じ方法で、NewsArticleTweet のインスタンスに対してトレイトメソッドを呼ぶことができます。唯一の違いは、ユーザーがトレイトと型の両方をスコープに持たなければならないことです。ここでは、バイナリクレートが私たちの aggregator ライブラリクレートをどのように使うかの例を示します。

use aggregator::{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 を出力します。

aggregator クレートに依存する他のクレートも、独自の型に対して Summary を実装するために、Summary トレイトをスコープに持つことができます。留意すべき制限は、トレイトまたは型のどちらか一方、または両方が私たちのクレートにローカルである場合にのみ、型に対してトレイトを実装できるということです。たとえば、Tweet のようなカスタム型に対して Display のような標準ライブラリのトレイトを実装することができます。これは、私たちの aggregator クレートの機能の一部として、型 Tweet が私たちの aggregator クレートにローカルであるためです。また、私たちの aggregator クレートの中で Vec<T> に対して Summary を実装することもできます。これは、トレイト Summary が私たちの aggregator クレートにローカルであるためです。

しかし、外部の型に対して外部のトレイトを実装することはできません。たとえば、私たちの aggregator クレートの中で Vec<T> に対して Display トレイトを実装することはできません。なぜなら、DisplayVec<T> の両方が標準ライブラリに定義されており、私たちの aggregator クレートにはローカルではないからです。この制限は、「整合性」と呼ばれる特性の一部であり、より具体的には「孤立則」と呼ばれています。この名前の由来は、親型が存在しないためです。このルールは、他人のコードが自分のコードを破壊しないように、逆も同様に保証します。このルールがなければ、2つのクレートが同じ型に対して同じトレイトを実装することができ、Rustはどの実装を使うかを知ることができません。

デフォルト実装

あるトレイトの一部またはすべてのメソッドに対して、すべての型に対するすべてのメソッドの実装を要求する代わりに、デフォルトの振る舞いを持つことが便利な場合があります。そして、特定の型に対してトレイトを実装する際には、各メソッドのデフォルトの振る舞いを維持または上書きすることができます。

リスト10-12で行ったように、メソッドのシグネチャのみを定義する代わりに、リスト10-14では、Summary トレイトの summarize メソッドに対してデフォルトの文字列を指定しています。

ファイル名: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

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

NewsArticle のインスタンスを要約するためにデフォルト実装を使用するには、impl Summary for NewsArticle {} という空の impl ブロックを指定します。

NewsArticle で直接 summarize メソッドを定義しなくなったとしても、デフォルト実装を提供し、NewsArticleSummary トレイトを実装することを指定しています。その結果、以下のように、NewsArticle のインスタンスでも依然として summarize メソッドを呼び出すことができます。

let article = 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.",
    ),
};

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

このコードは New article available! (Read more...) を出力します。

デフォルト実装を作成することで、リスト10-13の Tweet に対する Summary の実装については何も変更する必要はありません。その理由は、デフォルト実装を上書きする構文が、デフォルト実装のないトレイトメソッドを実装する構文と同じであるためです。

デフォルト実装は、同じトレイト内の他のメソッドを呼び出すことができます。たとえそれらの他のメソッドにデフォルト実装がなくてもです。このように、トレイトは多くの便利な機能を提供し、実装者にその一部のみを指定するように要求するだけです。たとえば、Summary トレイトに、実装が必要な summarize_author メソッドを持たせ、その後、summarize_author メソッドを呼び出すデフォルト実装を持つ summarize メソッドを定義することができます。

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

    fn summarize(&self) -> String {
        format!(
            "(Read more from {}...)",
            self.summarize_author()
        )
    }
}

このバージョンの Summary を使用するには、型に対してトレイトを実装する際に summarize_author のみを定義すればよいです。

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

summarize_author を定義した後、Tweet 構造体のインスタンスで summarize を呼び出すことができ、summarize のデフォルト実装が私たちが提供した summarize_author の定義を呼び出します。summarize_author を実装したため、Summary トレイトは、私たちにもうコードを書かなくても summarize メソッドの振る舞いを与えてくれます。これがどのように見えるかは以下の通りです。

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...) を出力します。

同じメソッドの上書き実装からデフォルト実装を呼び出すことはできないことに注意してください。

パラメータとしてのトレイト

トレイトの定義と実装方法を学んだので、トレイトを使って多くの異なる型を受け付ける関数を定義する方法を探ってみましょう。リスト10-13で NewsArticle 型と Tweet 型に対して実装した Summary トレイトを使って、notify 関数を定義します。この関数は、Summary トレイトを実装したある型の item パラメータに対して summarize メソッドを呼び出します。これを行うには、次のように impl Trait 構文を使います。

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

item パラメータの具体的な型の代わりに、impl キーワードとトレイト名を指定します。このパラメータは、指定されたトレイトを実装する任意の型を受け付けます。notify の本体では、Summary トレイトに属する item の任意のメソッド、たとえば summarize を呼び出すことができます。notify を呼び出して、NewsArticle または Tweet の任意のインスタンスを渡すことができます。Stringi32 などの他の型で関数を呼び出すコードはコンパイルされません。なぜなら、それらの型は Summary を実装していないからです。

トレイト境界構文

impl Trait 構文は単純なケースでは機能しますが、実際には「トレイト境界」と呼ばれる長い形式のシンタックスシュガーです。このようになります。

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 の引数として渡される値の具体的な型は同じでなければなりません。

+ 構文を使った複数のトレイト境界の指定

複数のトレイト境界を指定することもできます。notifyitem に対して summarize の他に表示形式を使用するようにしたい場合、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構文を使って、トレイトを実装するある型の値を返すことができます。次のようになります。

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構文を使うと、反復子トレイトを実装するある型を関数が返すことを簡潔に指定でき、非常に長い型を書き出す必要がなくなります。

ただし、戻り値が1つの型の場合にのみimpl Traitを使うことができます。たとえば、戻り値の型をimpl Summaryと指定してNewsArticleまたはTweetを返すこのコードは動作しません。

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,
        }
    }
}

コンパイラにおけるimpl Trait構文の実装方法に関する制限のため、NewsArticleまたはTweetを返すことは許可されていません。「異なる型の値を許容するトレイトオブジェクトの使用」でこの動作を持つ関数を書く方法について説明します。

トレイト境界を使って条件付きでメソッドを実装する

ジェネリック型パラメータを使ったimplブロックにトレイト境界を使うことで、指定されたトレイトを実装する型に対して条件付きでメソッドを実装することができます。たとえば、リスト10-15のPair<T>型は常にnew関数を実装してPair<T>の新しいインスタンスを返します(「メソッドの定義」で思い出してください。Selfimplブロックの型のエイリアスで、この場合Pair<T>です)。しかし、次のimplブロックでは、Pair<T>は内部型Tが比較を可能にするPartialOrdトレイトと印刷を可能にするDisplayトレイトの両方を実装している場合にのみcmp_displayメソッドを実装します。

ファイル名: src/lib.rs

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-15: トレイト境界に応じてジェネリック型に条件付きでメソッドを実装する

また、任意の型に対して、別のトレイトを実装している場合に条件付きでトレイトを実装することもできます。トレイト境界を満たす任意の型に対するトレイトの実装は、「ブランケット実装」と呼ばれ、Rust標準ライブラリで広く使用されています。たとえば、標準ライブラリはDisplayトレイトを実装する任意の型に対してToStringトレイトを実装しています。標準ライブラリのimplブロックはこのコードに似ています。

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

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

let s = 3.to_string();

ブランケット実装は、「実装者」セクションのトレイトのドキュメントに表示されます。

トレイトとトレイト境界を使うことで、ジェネリック型パラメータを使って重複を減らしながらコードを書くことができますが、また、ジェネリック型が特定の動作を持つことをコンパイラに指定することもできます。その後、コンパイラはトレイト境界情報を使って、コードで使用されるすべての具体的な型が正しい動作を提供していることを確認することができます。動的型付け言語では、型に定義されていないメソッドを呼び出した場合、実行時にエラーが発生します。しかし、Rustはこれらのエラーをコンパイル時に移動させるため、コードが実行される前に問題を修正するように強制されます。また、実行時に動作をチェックするコードを書く必要がなくなります。なぜなら、コンパイル時に既にチェック済みだからです。これにより、ジェネリクスの柔軟性を犠牲にすることなくパフォーマンスが向上します。

まとめ

おめでとうございます!「トレイト: 共有動作の定義」の実験を完了しました。LabExでさらに実験を行って、スキルを向上させることができます。