トレイト: 共通の振る舞いを定義する
トレイトにより、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; } #}
リスト10-12: summarizeメソッドで提供される振る舞いからなるSummaryトレイト
ここでは、traitキーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、
Summaryです。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、
今回の場合は、fn summarize(&self) -> Stringです。
メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。
このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。
コンパイラにより、Summaryトレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarizeが定義されていることが、
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに列挙され、 各行はセミコロンで終止します。
トレイトを型に実装する
今やSummaryトレイトで欲しい振る舞いを定義したので、メディア総括機で型に実装することができます。
リスト10-13は見出し、著者、場所を使用してsummarizeの戻り値を生成するNewsArticle構造体のSummaryトレイト実装を示しています。
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) } } #}
リスト10-13: SummaryトレイトをNewsArticleとTweet型に実装する
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、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つは、トレイトか対象の型が自分のクレートにローカルである時のみ、
型に対してトレイトを実装できるということです。例えば、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...)") } } #}
リスト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"),
// ピッツバーグ・ペンギンが再度NHLで最強のホッケーチームになった
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です。
ジェネリックな型Tがメソッドsummarizeを実装しないというエラーを出さずにitemに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); }
リスト10-15: PartialOrdとCopyトレイトを実装するあらゆるジェネリックな型に対して動く、
largest関数の動く定義
もし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); } } } #}
リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(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はこの種のエラーをコンパイル時に移したので、コードが動かせるようにさえなる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時に振る舞いがあるかどう確認するコードを書かなくても済みます。 そうすることでジェネリクスの柔軟性を諦める必要なく、パフォーマンスを向上させます。
もう使用したことのある別の種のジェネリクスは、ライフタイムと呼ばれます。 型が欲しい振る舞いを保持していることを保証するのではなく、必要な間だけ参照が有効であることをライフタイムは保証します。 ライフタイムがどうやってそれを行うかを見ましょう。