トレイト: 共通の振る舞いを定義する
トレイトは、特定の型に存在し、他の型と共有できる機能を定義します。 トレイトを使用すると、共通の振る舞いを抽象的に定義できます。トレイト境界を使用すると、 あるジェネリック型が、特定の振る舞いをもつあらゆる型になり得ることを指定できます。
注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全てに対して同じメソッドを呼び出せるなら、 同じ振る舞いを共有することになります。トレイト定義は、メソッドシグニチャをあるグループにまとめ、なんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。
例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所から送られる新しいニュースを保持する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が定義されていることが
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに並べられ、 各行はセミコロンで終わります。
トレイトを型に実装する
これで Summary トレイトのメソッドのシグネチャを希望通りに定義できたので、メディア アグリゲータ内の型に対してこれを実装できます。
リスト10-13は、 Summary トレイトを NewsArticle 構造体上に実装したもので、ヘッドライン、著者、そして地域情報を使ってsummarize の戻り値を作っています。
Tweet 構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize を定義します。
ファイル名: src/lib.rs
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に対して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トレイトをスコープに持ち込むことができます。
注意すべき制限の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
pub trait Summary {
fn summarize(&self) -> String {
// "(もっと読む)"
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
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-14: summarizeメソッドのデフォルト実装があるSummaryトレイトを定義する
デフォルト実装を利用してNewsArticleのインスタンスをまとめるには、
impl Summary for NewsArticle {}と空のimplブロックを指定します。
もはやNewsArticleに直接summarizeメソッドを定義してはいませんが、私達はデフォルト実装を提供しており、
NewsArticleはSummaryトレイトを実装すると指定しました。そのため、
NewsArticleのインスタンスに対してsummarizeメソッドを同じように呼び出すことができます。
このように:
use aggregator::{self, NewsArticle, Summary};
fn main() {
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...)(新しい記事があります!(もっと読む))と出力します。
デフォルト実装を用意しても、リスト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())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
このバージョンのSummaryを使用するために、型にトレイトを実装する際、実装する必要があるのはsummarize_authorだけです:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
// "({}さんの文章をもっと読む)"
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summarize_author定義後、Tweet構造体のインスタンスに対してsummarizeを呼び出せ、
summarizeのデフォルト実装は、私達が提供したsummarize_authorの定義を呼び出すでしょう。
summarize_authorを実装したので、追加のコードを書く必要なく、Summaryトレイトは、
summarizeメソッドの振る舞いを与えてくれました。
use aggregator::{self, 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: (Read more from @horse_ebooks...)(1つの新しいツイート:(@horse_ebooksさんの文章をもっと読む))と出力します。
デフォルト実装を、そのメソッドをオーバーライドしている実装から呼び出すことはできないことに注意してください。
引数としてのトレイト
トレイトを定義し実装する方法はわかったので、トレイトを使っていろんな種類の型を受け付ける関数を定義する方法を学んでいきましょう。
リスト10-13でNewsArticleとTweetに対して実装したSummaryトレイトを使用して、notify関数を定義しましょう。
この関数は、Summaryトレイトを実装する何らかの型を持つ引数itemを持ち、それに対してsummarizeメソッドを呼び出します。
これを行うためには、このようにimpl Trait構文を使います:
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)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
引数の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に、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をフォーマットすることの両方が行なえます。
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,
{
unimplemented!()
}
この関数シグニチャは、よりさっぱりとしています。トレイト境界を多く持たない関数と同じように、関数名、引数リスト、戻り値の型が一緒になって近くにあるからですね。
トレイトを実装している型を返す
以下のように、impl Trait構文を戻り値型のところで使うことにより、あるトレイトを実装する何らかの型を返すことができます。
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)
}
}
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構文を使えば、非常に長い型を書くことなく、ある関数はIteratorトレイトを実装するある型を返すのだ、と簡潔に指定することができます。
ただし、impl Traitは一種類の型を返す場合にのみ使えます。
たとえば、以下のように、戻り値の型はimpl Summaryで指定しつつ、NewsArticleかTweetを返すようなコードは失敗します:
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)
}
}
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,
}
}
}
NewsArticleかTweetを返すというのは、コンパイラのimpl Trait構文の実装まわりの制約により許されていません。
このような振る舞いをする関数を書く方法は、17章の「トレイトオブジェクトで異なる型の値を許容する」節で学びます。
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つimplブロックにトレイト境界を与えることで、
特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、
リスト10-15の型Pair<T>は、Pair<T>の新しいインスタンスを返すnew関数を常に実装します
(第5章の「メソッドを定義する」節で学んだ、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: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、
Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、
Displayトレイトを実装するあらゆる型にToStringトレイトを実装しています。
標準ライブラリのimplブロックは以下のような見た目です:
impl<T: Display> ToString for T {
// --snip--
}
標準ライブラリにはこのブランケット実装があるので、Displayトレイトを実装する任意の型に対して、
ToStringトレイトで定義されたto_stringメソッドを呼び出せるのです。
例えば、整数はDisplayを実装するので、このように整数値を対応するString値に変換できます:
#![allow(unused)] fn main() { let s = 3.to_string(); }
ブランケット実装は、トレイトのドキュメンテーションの「実装したもの」節に出現します。
トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付き言語では、その型に定義されていないメソッドを呼び出せば、実行時 (runtime) にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようになる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時の振る舞いを確認するコードを書かなくても済みます。 そうすることで、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させます。