トレイト: 共通の振る舞いを定義する
トレイトにより、Rustコンパイラに特定の型に存在し、他の型と共有できる機能について知らせます。 トレイトを使用して共通の振る舞いを抽象的に定義できます。トレイト境界を使用して、 あるジェネリックが特定の振る舞いのあるあらゆる型になり得ることを指定できます。
注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全部に対して同じメソッドを呼び出せたら、 同じ振る舞いを共有します。トレイト定義は、メソッドシグニチャを一緒くたにしてなんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。
例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所で送られる新しいニュースを保持するNewsArticle
と、
新規ツイートか、リツイートか、はたまた他のツイートへのリプライなのかを示すメタデータを伴う最大で280文字までのTweet
です。
NewsArticle
または Tweet
インスタンスに保存されているデータのサマリを表示できるメディア アグリゲータ ライブラリを作成します。
これをするには、各型のサマリーが必要で、インスタンスで summarize
メソッドを呼び出してサマリーを要求する必要があります。
リスト10-12は、この振る舞いを表現するSummary
トレイトの定義を表示しています。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub trait Summary { fn summarize(&self) -> String; } #}
ここでは、trait
キーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、
Summary
です。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、
今回の場合は、fn summarize(&self) -> String
です。
メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。
このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。
コンパイラにより、Summary
トレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarize
が定義されていることが、
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに列挙され、 各行はセミコロンで終止します。
トレイトを型に実装する
今や Summary
トレイトを使用して目的の動作を定義できたので、メディア アグリゲータで型に実装できます。
リスト10-13は、 Summary
トレイトを NewsArticle
構造体上に実装したもので、ヘッドライン、著者、そして summarize
の戻り値を示しています。
Tweet
構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize
を定義します。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #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) } } #}
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、impl
の後に、
実装したいトレイトの名前を置き、それからfor
キーワード、さらにトレイトの実装対象の型の名前を指定することです。
impl
ブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、
波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。
トレイトを実装後、普通のメソッド同様にNewsArticle
やTweet
のインスタンスに対してこのメソッドを呼び出せます。
こんな感じで:
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
トレイトと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
# #![allow(unused_variables)] #fn main() { pub trait Summary { fn summarize(&self) -> String { // (もっと読む) String::from("(Read more...)") } } #}
独自の実装を定義するのではなく、デフォルト実装を使用して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"),
// ピッツバーグ・ペンギンが再度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のTweet
のSummary
実装を変える必要はありません。
理由は、デフォルト実装をオーバーライドする記法がデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。
デフォルト実装は、他のデフォルト実装がないメソッドでも呼び出すことができます。
このように、トレイトは多くの有用な機能を提供しつつ、実装者に僅かな部分だけ指定してもらう必要しかないのです。
例えば、Summary
トレイトを実装が必須のsummarize_author
メソッドを持つように定義し、
それからsummarize_author
メソッドを呼び出すデフォルト実装のあるsummarize
メソッドを定義することもできます:
# #![allow(unused_variables)] #fn main() { 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で、Summary
トレイトを型NewsArticle
とTweet
に実装しました。
引数item
に対してsummarize
メソッドを呼び出す関数notify
を定義でき、この引数はジェネリックな型T
です。
item
のsummarize
を呼ぶときにジェネリックな型T
がメソッドsummarize
を実装してないというエラーが出ないように、
T
のトレイト境界を使ってitem
がSummary
トレイトを実装する型でなければならないと指定できます。
pub fn notify<T: Summary>(item: T) {
// 新ニュース! {}
println!("Breaking news! {}", item.summarize());
}
トレイト境界をジェネリックな型引数宣言とともにコロンの後、山カッコ内に配置しています。T
に付けられたトレイト境界のため、
notify
を呼び出してNewsArticle
かTweet
のどんなインスタンスも渡すことができます。
あらゆる他の型、String
やi32
などでこの関数を呼び出すコードは、型がSummary
を実装しないので、
コンパイルできません。
+
記法でジェネリックな型に複数のトレイト境界を指定できます。例えば、関数でT
に対してフォーマット表示と、
summarize
メソッドを使用するには、T: Summary + Display
を使用して、T
はSummary
とDisplay
を実装するどんな型にもなると宣言できます。
しかしながら、トレイト境界が多すぎると欠点もあります。各ジェネリックには、特有のトレイト境界があるので、
複数のジェネリックな型引数がある関数には、関数名と引数リストの間に多くのトレイト境界の情報が付くこともあり、
関数シグニチャが読みづらくなる原因になります。このため、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
{
この関数シグニチャは、多くのトレイト境界のない関数のように、関数名、引数リスト、戻り値の型が一緒になって近いという点でごちゃごちゃしていません。
トレイト境界でlargest
関数を修正する
ジェネリックな型引数の境界で使用したい振る舞いを指定する方法を知ったので、リスト10-5に戻って、
ジェネリックな型引数を使用するlargest
関数の定義を修正しましょう!最後にそのコードを実行しようとしたら、
こんなエラーが出ました:
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
largest
の本体で、大なり演算子(>
)を使用して型T
の2つの値を比較したかったのです。その演算子は、
標準ライブラリトレイトのstd::cmp::PartialOrd
でデフォルトメソッドとして定義されているので、
largest
関数が、比較できるあらゆる型のスライスに対して動くように、T
のトレイト境界にPartialOrd
を指定する必要があります。
初期化処理に含まれているので、PartialOrd
をスコープに導入する必要はありません。
largest
のシグニチャを以下のような見た目に変えてください:
fn largest<T: PartialOrd>(list: &[T]) -> T {
今度コードをコンパイルすると、異なる一連のエラーが出ます:
error[E0508]: cannot move out of type `[T]`, a non-copy slice
(エラー: `[T]`、コピーでないスライスからムーブできません。)
--> src/main.rs:2:23
|
2 | let mut largest = list[0];
| ^^^^^^^
| |
| cannot move out of here
| help: consider using a reference instead: `&list[0]`
(助言: 代わりに参照の使用を考慮してください: `&list[0]`)
error[E0507]: cannot move out of borrowed content
(エラー: 借用された内容からムーブできません)
--> src/main.rs:4:9
|
4 | for &item in list.iter() {
| ^----
| ||
| |hint: to prevent move, use `ref item` or `ref mut item`
| cannot move out of borrowed content
(ヒント: ムーブを避けるには、`ref item`か`ref mut item`を使用してください)
このエラーの鍵となる行は、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.iter() { 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); }
もしlargest
関数をCopy
を実装する型だけに制限したくなかったら、Copy
ではなく、
T
がClone
というトレイト境界を持つと指定することもできます。そうしたら、
largest
関数に所有権が欲しい時にスライスの各値をクローンできます。clone
関数を使用するということは、
String
のようなヒープデータを所有する型の場合にもっとヒープ確保が発生する可能性があることを意味し、
大きなデータを取り扱っていたら、ヒープ確保は遅いこともあります。
largest
の別の実装方法は、関数がスライスのT
値への参照を返すようにすることです。
戻り値の型をT
ではなく&T
に変え、それにより関数の本体を参照を返すように変更したら、
Clone
かCopy
トレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。
試しにこれらの対立的な解決策もご自身で実装してみてください!
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つimpl
ブロックにトレイト境界を与えることで、
特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、
リスト10-16の型Pair<T>
は、常にnew
関数を実装します。しかし、Pair<T>
は、
内部の型T
が比較を可能にするPartialOrd
トレイトと出力を可能にするDisplay
トレイトを実装している時のみ、
cmp_display
メソッドを実装します。
# #![allow(unused_variables)] #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); } } } #}
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、
Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、
Display
トレイトを実装するあらゆる型にToString
トレイトを実装しています。
標準ライブラリのimpl
ブロックは以下のような見た目です:
impl<T: Display> ToString for T {
// --snip--
}
標準ライブラリにはこのブランケット実装があるので、Display
トレイトを実装する任意の型に対して、
ToString
トレイトで定義されたto_string
メソッドを呼び出せるのです。
例えば、整数はDisplay
を実装するので、このように整数値を対応するString
値に変換できます:
# #![allow(unused_variables)] #fn main() { let s = 3.to_string(); #}
ブランケット実装は、「実装したもの」節のトレイトのドキュメンテーションに出現します。
トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付け言語では、型が実装しない型のメソッドを呼び出せば、実行時にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようにさえなる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時に振る舞いがあるかどうか確認するコードを書かなくても済みます。 そうすることでジェネリクスの柔軟性を諦める必要なく、パフォーマンスを向上させます。
もう使用してきたことのある別の種のジェネリクスは、ライフタイムと呼ばれます。 型が欲しい振る舞いを保持していることを保証するのではなく、必要な間だけ参照が有効であることをライフタイムは保証します。 ライフタイムがどうやってそれを行うかを見ましょう。