トレイト: 共通の振る舞いを定義する
トレイトは、Rustコンパイラに、特定の型に存在し、他の型と共有できる機能について知らせます。 トレイトを使用すると、共通の振る舞いを抽象的に定義できます。トレイト境界を使用すると、 あるジェネリックが、特定の振る舞いをもつあらゆる型になり得ることを指定できます。
注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全てに対して同じメソッドを呼び出せるなら、 同じ振る舞いを共有することになります。トレイト定義は、メソッドシグニチャをあるグループにまとめ、なんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。
例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所から送られる新しいニュースを保持するNewsArticle
と、
新規ツイートか、リツイートか、はたまた他のツイートへのリプライなのかを示すメタデータを伴う最大で280文字までのTweet
です。
NewsArticle
または Tweet
インスタンスに保存されているデータのサマリーを表示できるメディア アグリゲータ ライブラリを作成します。
これをするには、各型のサマリーが必要で、インスタンスで summarize
メソッドを呼び出してサマリーを要求する必要があります。
リスト10-12は、この振る舞いを表現するSummary
トレイトの定義を表示しています。
ファイル名: src/lib.rs
リスト10-12: summarize
メソッドで提供される振る舞いからなるSummary
トレイト
ここでは、trait
キーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、
Summary
です。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、
今回の場合は、fn summarize(&self) -> String
です。
メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。
このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。
コンパイラにより、Summary
トレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarize
が定義されていることが
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに並べられ、 各行はセミコロンで終わります。
トレイトを型に実装する
今や Summary
トレイトを使用して目的の動作を定義できたので、メディア アグリゲータでこれを型に実装できます。
リスト10-13は、 Summary
トレイトを NewsArticle
構造体上に実装したもので、ヘッドライン、著者、そして地域情報を使ってsummarize
の戻り値を作っています。
Tweet
構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize
を定義します。
ファイル名: src/lib.rs
リスト10-13: Summary
トレイトをNewsArticle
とTweet
型に実装する
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、impl
の後に、
実装したいトレイトの名前を置き、それからfor
キーワード、さらにトレイトの実装対象の型の名前を指定することです。
impl
ブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、
波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。
トレイトを実装後、普通のメソッド同様にNewsArticle
やTweet
のインスタンスに対してこのメソッドを呼び出せます。
こんな感じで:
このコードは、1 new tweet: horse_ebooks: of course, as you probably already know, people
と出力します。
リスト10-13でSummary
トレイトとNewArticle
、Tweet
型を同じlib.rsに定義したので、
全部同じスコープにあることに注目してください。このlib.rsをaggregator
と呼ばれるクレート専用にして、
誰か他の人が私たちのクレートの機能を活用して自分のライブラリのスコープに定義された構造体にSummary
トレイトを実装したいとしましょう。
まず、トレイトをスコープに取り込む必要があるでしょう。use aggregator::Summary;
と指定してそれを行えば、
これにより、自分の型にSummary
を実装することが可能になるでしょう。Summary
トレイトは、
他のクレートが実装するためには、公開トレイトである必要があり、ここでは、リスト10-12のtrait
の前に、
pub
キーワードを置いたのでそうなっています。
トレイト実装で注意すべき制限の1つは、トレイトか対象の型が自分のクレートに固有(local)である時のみ、
型に対してトレイトを実装できるということです。例えば、Display
のような標準ライブラリのトレイトをaggregator
クレートの機能の一部として、
Tweet
のような独自の型に実装できます。型Tweet
がaggregator
クレートに固有だからです。
また、Summary
をaggregator
クレートでVec<T>
に対して実装することもできます。
トレイトSummary
は、aggregator
クレートに固有だからです。
しかし、外部のトレイトを外部の型に対して実装することはできません。例として、
aggregator
クレート内でVec<T>
に対してDisplay
トレイトを実装することはできません。
Display
とVec<T>
は標準ライブラリで定義され、aggregator
クレートに固有ではないからです。
この制限は、コヒーレンス(coherence)、特に孤児のルール(orphan rule)と呼ばれるプログラムの特性の一部で、
親の型が存在しないためにそう命名されました。この規則により、他の人のコードが自分のコードを壊したり、
その逆が起きないことを保証してくれます。この規則がなければ、2つのクレートが同じ型に対して同じトレイトを実装できてしまい、
コンパイラはどちらの実装を使うべきかわからなくなってしまうでしょう。
デフォルト実装
時として、全ての型の全メソッドに対して実装を要求するのではなく、トレイトの全てあるいは一部のメソッドに対してデフォルトの振る舞いがあると有用です。 そうすれば、特定の型にトレイトを実装する際、各メソッドのデフォルト実装を保持するかオーバーライドするか選べるわけです。
リスト10-14は、リスト10-12のように、メソッドシグニチャだけを定義するのではなく、
Summary
トレイトのsummarize
メソッドにデフォルトの文字列を指定する方法を示しています。
ファイル名: src/lib.rs
リスト10-14: summarize
メソッドのデフォルト実装があるSummary
トレイトの定義
独自の実装を定義するのではなく、デフォルト実装を利用してNewsArticle
のインスタンスをまとめるには、
impl Summary for NewsArticle {}
と空のimpl
ブロックを指定します。
もはやNewsArticle
に直接summarize
メソッドを定義してはいませんが、私達はデフォルト実装を提供しており、
NewsArticle
はSummary
トレイトを実装すると指定しました。そのため、
NewsArticle
のインスタンスに対してsummarize
メソッドを同じように呼び出すことができます。
このように:
このコードは、New article available! (Read more...)
(新しい記事があります!(もっと読む)
)と出力します。
summarize
にデフォルト実装を用意しても、リスト10-13のTweet
のSummary
実装を変える必要はありません。
理由は、デフォルト実装をオーバーライドする記法はデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。
デフォルト実装は、自らのトレイトのデフォルト実装を持たない他のメソッドを呼び出すことができます。
このようにすれば、トレイトは多くの有用な機能を提供しつつ、実装者は僅かな部分しか指定しなくて済むようになります。
例えば、Summary
トレイトを、(実装者が)内容を実装しなければならないsummarize_author
メソッドを持つように定義し、
それからsummarize_author
メソッドを呼び出すデフォルト実装を持つsummarize
メソッドを定義することもできます:
このバージョンのSummary
を使用するために、型にトレイトを実装する際、実装する必要があるのはsummarize_author
だけです:
summarize_author
定義後、Tweet
構造体のインスタンスに対してsummarize
を呼び出せ、
summarize
のデフォルト実装は、私達が提供したsummarize_author
の定義を呼び出すでしょう。
summarize_author
を実装したので、追加のコードを書く必要なく、Summary
トレイトは、
summarize
メソッドの振る舞いを与えてくれました。
このコードは、1 new tweet: (Read more from @horse_ebooks...)
(1つの新しいツイート:(@horse_ebooksさんの文章をもっと読む)
)と出力します。
デフォルト実装を、そのメソッドをオーバーライドしている実装から呼び出すことはできないことに注意してください。
引数としてのトレイト
トレイトを定義し実装する方法はわかったので、トレイトを使っていろんな種類の型を受け付ける関数を定義する方法を学んでいきましょう。
たとえば、Listing 10-13では、NewsArticle
とTweet
型にSummary
トレイトを実装しました。
ここで、引数のitem
のsummarize
メソッドを呼ぶ関数notify
を定義することができます。ただし、引数item
はSummary
トレイトを実装しているような何らかの型であるとします。
このようなことをするためには、impl Trait
構文を使うことができます。
引数のitem
には、具体的な型の代わりに、impl
キーワードとトレイト名を指定します。
この引数は、指定されたトレイトを実装しているあらゆる型を受け付けます。
notify
の中身では、summarize
のような、Summary
トレイトに由来するitem
のあらゆるメソッドを呼び出すことができます。
私達は、notify
を呼びだし、NewsArticle
かTweet
のどんなインスタンスでも渡すことができます。
この関数を呼び出すときに、String
やi32
のような他の型を渡すようなコードはコンパイルできません。
なぜなら、これらの型は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) {
この関数が受け取るitem1
とitem2
の型が(どちらもSummary
を実装する限り)異なっても良いとするならば、impl Trait
は適切でしょう。
両方の引数が同じ型であることを強制することは、以下のようにトレイト境界を使ってのみ表現可能です:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
引数であるitem1
とitem2
の型としてジェネリックな型T
を指定しました。
これにより、item1
とitem2
として関数に渡される値の具体的な型が同一でなければならない、という制約を与えています。
複数のトレイト境界を+
構文で指定する
複数のトレイト境界も指定できます。
たとえば、notify
にsummarize
メソッドに加えてitem
の画面出力形式(ディスプレイフォーマット)を使わせたいとします。
その場合は、notify
の定義にitem
はDisplay
とSummary
の両方を実装していなくてはならないと指定することになります。
これは、以下のように+
構文で行うことができます:
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
構文を戻り値型のところで使うことにより、あるトレイトを実装する何らかの型を返すことができます。
戻り値の型としてimpl Summary
を使うことにより、具体的な型が何かを言うことなく、returns_summarizable
関数はSummary
トレイトを実装している何らかの型を返すのだ、と指定することができます。
今回returns_summarizable
はTweet
を返しますが、この関数を呼び出すコードはそのことを知りません。
実装しているトレイトだけで戻り値型を指定できることは、13章で学ぶ、クロージャとイテレータを扱うときに特に便利です。
クロージャとイテレータの作り出す型は、コンパイラだけが知っているものであったり、指定するには長すぎるものであったりします。
impl Trait
構文を使えば、非常に長い型を書くことなく、ある関数はIterator
トレイトを実装するある型を返すのだ、と簡潔に指定することができます。
ただし、impl Trait
は一種類の型を返す場合にのみ使えます。
たとえば、以下のように、戻り値の型はimpl Summary
で指定しつつ、NewsArticle
かTweet
を返すようなコードは失敗します:

NewsArticle
かTweet
を返すというのは、コンパイラの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
のシグニチャを以下のように変えてください:
今回のコンパイルでは、別のエラーが出てきます:
$ 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
関数では、最大のi32
かchar
を探そうとするだけでした。
第4章のスタックのみのデータ: コピー節で議論したように、i32
やchar
のようなサイズが既知の型は
スタックに格納できるので、Copy
トレイトを実装しています。しかし、largest
関数をジェネリックにすると、
list
引数がCopy
トレイトを実装しない型を含む可能性も出てきたのです。結果として、
list[0]
から値をlargest
にムーブできず、このエラーに陥ったのです。
このコードをCopy
トレイトを実装する型だけを使って呼び出すようにしたいなら、T
のトレイト境界にCopy
を追加すればよいです!
リスト10-15は、関数に渡したスライスの値の型が、i32
やchar
などのようにPartialOrd
とCopy
を実装する限りコンパイルできる、ジェネリックな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: PartialOrd
とCopy
トレイトを実装するあらゆるジェネリックな型に対して動く、
largest
関数の実際の定義
もしlargest
関数をCopy
を実装する型だけに制限したくなかったら、T
がCopy
ではなくClone
というトレイト境界を持つと指定することもできます。そうしたら、
largest
関数に所有権が欲しい時にスライスの各値をクローンできます。clone
関数を使用するということは、
String
のようなヒープデータを持つ型の場合により多くのヒープ確保が発生する可能性があることを意味します。
そして、大量のデータを取り扱っていたら、ヒープ確保には時間がかかることもあります。
largest
の別の実装方法は、関数がスライスのT
値への参照を返すようにすることです。
戻り値の型をT
ではなく&T
に変え、それにより関数の本体を参照を返すように変更したら、
Clone
やCopy
トレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。
これらの代替策をご自身で実装してみましょう!
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つimpl
ブロックにトレイト境界を与えることで、
特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、
リスト10-16の型Pair<T>
は、常にnew
関数を実装します。しかし、Pair<T>
は、
内部の型T
が比較を可能にするPartialOrd
トレイトと出力を可能にするDisplay
トレイトを実装している時のみ、
cmp_display
メソッドを実装します。
ファイル名: src/lib.rs
リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、
Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、
Display
トレイトを実装するあらゆる型にToString
トレイトを実装しています。
標準ライブラリのimpl
ブロックは以下のような見た目です:
impl<T: Display> ToString for T {
// --snip--
}
標準ライブラリにはこのブランケット実装があるので、Display
トレイトを実装する任意の型に対して、
ToString
トレイトで定義されたto_string
メソッドを呼び出せるのです。
例えば、整数はDisplay
を実装するので、このように整数値を対応するString
値に変換できます:
ブランケット実装は、トレイトのドキュメンテーションの「実装したもの」節に出現します。
トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付き言語では、その型に定義されていないメソッドを呼び出せば、実行時 (runtime) にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようになる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時の振る舞いを確認するコードを書かなくても済みます。 そうすることで、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させます。
すでに使っている他のジェネリクスに、ライフタイムと呼ばれるものがあります。 ライフタイムは、型が欲しい振る舞いを保持していることではなく、必要な間だけ参照が有効であることを保証します。 ライフタイムがどうやってそれを行うかを見てみましょう。