はじめに
トレイト: 共有振る舞いの定義へようこそ。この実験は、Rust ブックの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、型における共有振る舞いを定義し、ジェネリック型に対するトレイト境界を指定するための方法としてトレイトを探ります。
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
ブロックの中には、トレイト定義で定義されたメソッドのシグネチャを書きます。各シグネチャの後にセミコロンを追加する代わりに、波括弧を使って、トレイトのメソッドが特定の型に対して持つべき特定の振る舞いでメソッド本体を埋めます。
ライブラリが NewsArticle
と Tweet
に対して Summary
トレイトを実装したので、クレートのユーザーは、通常のメソッドを呼ぶのと同じ方法で、NewsArticle
と Tweet
のインスタンスに対してトレイトメソッドを呼ぶことができます。唯一の違いは、ユーザーがトレイトと型の両方をスコープに持たなければならないことです。ここでは、バイナリクレートが私たちの 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
トレイトを実装することはできません。なぜなら、Display
と Vec<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
メソッドを定義しなくなったとしても、デフォルト実装を提供し、NewsArticle
が Summary
トレイトを実装することを指定しています。その結果、以下のように、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
の任意のインスタンスを渡すことができます。String
や i32
などの他の型で関数を呼び出すコードはコンパイルされません。なぜなら、それらの型は 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) {
この関数が item1
と item2
に異なる型を許す(両方の型が Summary
を実装している限り)場合、impl Trait
を使用するのが適切です。ただし、両方のパラメータが同じ型であることを強制したい場合は、次のようにトレイト境界を使用する必要があります。
pub fn notify<T: Summary>(item1: &T, item2: &T) {
item1
と item2
のパラメータの型として指定されたジェネリック型 T
は、関数を制約します。つまり、item1
と item2
の引数として渡される値の具体的な型は同じでなければなりません。
複数のトレイト境界を指定することもできます。notify
が item
に対して summarize
の他に表示形式を使用するようにしたい場合、notify
の定義で item
が Display
と Summary
の両方を実装する必要があることを指定します。これは +
構文を使って行うことができます。
pub fn notify(item: &(impl Summary + Display)) {
+
構文は、ジェネリック型のトレイト境界でも有効です。
pub fn notify<T: Summary + Display>(item: &T) {
2つのトレイト境界が指定されると、notify
の本体は summarize
を呼び出し、{}
を使って item
をフォーマットすることができます。
トレイト境界をたくさん使うと欠点があります。各ジェネリックには独自のトレイト境界があるため、複数のジェネリック型パラメータを持つ関数は、関数名とパラメータリストの間に大量のトレイト境界情報を含めることができ、関数のシグネチャを読みにくくします。そのため、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_summarizable
はTweet
を返しますが、この関数を呼び出すコードはそれを知る必要はありません。
実装するトレイトだけで戻り値の型を指定できる機能は、第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>
の新しいインスタンスを返します(「メソッドの定義」で思い出してください。Self
はimpl
ブロックの型のエイリアスで、この場合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でさらに実験を行って、スキルを向上させることができます。