トレイトを使って共有の振る舞いを定義する
トレイト は、ある特定の型が持ち、他の型と共有できる機能を定義します。トレイトを使うと、共有される振る舞いを抽象的な方法で定義できます。トレイト境界 を使うと、ジェネリック型が特定の振る舞いを持つ任意の型になりうることを指定できます。
注: トレイトは、他の言語でしばしば インターフェース と呼ばれる機能に似ていますが、いくつか違いがあります。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドで構成されます。同じメソッドをそれらすべての型に対して呼び出せるなら、異なる型は同じ振る舞いを共有していることになります。トレイト定義は、ある目的を達成するために必要な一連の振る舞いを定義するため、メソッドシグネチャをひとまとめにする方法です。
たとえば、さまざまな種類と量のテキストを保持する複数の構造体があるとしましょう。ある特定の場所で報告されたニュース記事を保持する NewsArticle 構造体と、最大 280 文字までのテキストに加えて、それが新規投稿、再投稿、または別の投稿への返信であるかを示すメタデータを持てる SocialPost です。
NewsArticle や SocialPost のインスタンスに格納されているかもしれないデータの要約を表示できる、aggregator という名前のメディアアグリゲータのライブラリクレートを作りたいとします。そのためには、それぞれの型から要約を取得する必要があり、その要約はインスタンスに対して summarize メソッドを呼び出すことで要求します。リスト 10-12 は、この振る舞いを表現する公開 Summary トレイトの定義を示しています。
pub trait Summary {
fn summarize(&self) -> String;
}
ここでは、trait キーワードに続けて、この場合は Summary であるトレイト名を書いてトレイトを宣言しています。また、このトレイトを pub として宣言しているので、このクレートに依存する他のクレートもこのトレイトを利用できます。これについては、後のいくつかの例で確認します。波かっこの内側では、このトレイトを実装する型の振る舞いを記述するメソッドシグネチャを宣言します。この場合は fn summarize(&self) -> String です。
メソッドシグネチャの後には、波かっこ内に実装を書く代わりに、セミコロンを使います。このトレイトを実装する各型は、メソッド本体に対して独自の振る舞いを提供しなければなりません。コンパイラは、Summary トレイトを持つあらゆる型に、このシグネチャどおりの summarize メソッドが定義されていることを強制します。
トレイトは本体に複数のメソッドを持つことができます。メソッドシグネチャは 1 行に 1 つずつ並び、各行はセミコロンで終わります。
型にトレイトを実装する
Summary トレイトのメソッドに望まれるシグネチャを定義したので、次はそれをメディアアグリゲータ内の型に実装できます。リスト 10-13 は、見出し、著者、場所を使って summarize の戻り値を作る、NewsArticle 構造体に対する Summary トレイトの実装を示しています。SocialPost 構造体については、投稿内容がすでに 280 文字に制限されていると仮定して、ユーザー名の後に投稿全文を続けるものとして summarize を定義します。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
型にトレイトを実装することは、通常のメソッドを実装するのと似ています。違いは、impl の後に実装したいトレイト名を書き、それから for キーワードを使い、その後にそのトレイトを実装したい型の名前を指定することです。impl ブロックの中には、トレイト定義で定義されたメソッドシグネチャを書きます。各シグネチャの後にセミコロンを付ける代わりに、波かっこを使い、その特定の型に対してトレイトのメソッドに持たせたい具体的な振る舞いをメソッド本体に記述します。
ライブラリが NewsArticle と SocialPost に対して Summary トレイトを実装したので、クレートの利用者は、通常のメソッドを呼び出すのと同じ方法で、NewsArticle と SocialPost のインスタンスに対してトレイトメソッドを呼び出せます。唯一の違いは、利用者が型だけでなくトレイトもスコープに導入しなければならないことです。以下は、バイナリクレートが私たちの aggregator ライブラリクレートを利用する例です。
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
このコードは 1 new post: horse_ebooks: of course, as you probably already know, people を出力します。
aggregator クレートに依存する他のクレートも、自分たちの型に Summary を実装するために Summary トレイトをスコープに導入できます。注意すべき制約が 1 つあります。それは、トレイトか型のいずれか一方、またはその両方が自分たちのクレートにローカルである場合にのみ、ある型に対してトレイトを実装できるということです。たとえば、SocialPost 型は私たちの aggregator クレートにローカルなので、aggregator クレートの機能の一部として、SocialPost のようなカスタム型に標準ライブラリの Display トレイトを実装できます。また、Summary トレイトは私たちの aggregator クレートにローカルなので、aggregator クレート内で Vec<T> に対して Summary を実装することもできます。
しかし、外部の型に対して外部のトレイトを実装することはできません。たとえば、Display と Vec<T> はどちらも標準ライブラリで定義されており、私たちの aggregator クレートにはローカルではないため、aggregator クレート内で Vec<T> に Display トレイトを実装することはできません。この制約は コヒーレンス と呼ばれる性質の一部であり、より具体的には 孤児ルール と呼ばれます。これは親となる型が存在しないことに由来する名前です。このルールにより、他人のコードがあなたのコードを壊したり、その逆が起きたりしないことが保証されます。このルールがなければ、2 つのクレートが同じ型に同じトレイトを実装できてしまい、どの実装を使うべきかを Rust は判断できなくなります。
デフォルト実装を使う
あるトレイト内の一部またはすべてのメソッドについて、すべての型で実装を必須にするのではなく、デフォルトの振る舞いを持たせることが便利な場合があります。そうすれば、そのトレイトを特定の型に実装するときに、各メソッドのデフォルトの振る舞いをそのまま使うことも、オーバーライドすることもできます。
リスト 10-14 では、リスト 10-12 のように Summary トレイトでメソッドシグネチャだけを定義するのではなく、summarize メソッドに対してデフォルトの文字列を指定しています。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
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"),
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 の SocialPost に対する
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summary_author を定義した後は、SocialPost 構造体のインスタンスに対して
summarize を呼び出せます。すると、summarize のデフォルト実装が
私たちの用意した summarize_author の定義を呼び出します。私たちが
summarize_author を実装しているので、Summary トレイトは
これ以上コードを書かなくても summarize メソッドの
振る舞いを与えてくれます。次のようになります:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
このコードは 1 new post: (Read more from @horse_ebooks...) を出力します。
なお、同じメソッドのオーバーライド実装から、そのメソッドの デフォルト実装を呼び出すことはできません。
パラメータとしてトレイトを使う
トレイトの定義と実装の方法がわかったので、次は
トレイトを使ってさまざまな型を受け取る関数を定義する方法を見ていきましょう。ここでは、
リスト 10-13 で NewsArticle 型と SocialPost 型に実装した
Summary トレイトを使って、item パラメータに対して summarize メソッドを
呼び出す notify 関数を定義します。この item は Summary
トレイトを実装する何らかの型です。これを行うには、次のように 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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
item パラメータに具体的な型を指定する代わりに、impl
キーワードとトレイト名を指定します。このパラメータは、指定された
トレイトを実装する任意の型を受け取ります。notify の本体では、item に対して
Summary トレイト由来の任意のメソッド、たとえば summarize を
呼び出せます。notify を呼び出して NewsArticle や SocialPost の任意の
インスタンスを渡すことができます。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) {
impl Trait を使うのは、この関数で item1 と
item2 に異なる型を許可したい場合に適しています(両方の型が Summary を実装している限り)。
しかし、両方のパラメータを同じ型にしたいのであれば、次のように
トレイト境界を使わなければなりません:
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 構文を使って、トレイトを実装する
何らかの型の値を返すことができます。次のようになります:
```rust,ignore
# 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 SocialPost {
# pub username: String,
# pub content: String,
# pub reply: bool,
# pub repost: bool,
# }
#
# impl Summary for SocialPost {
# fn summarize(&self) -> String {
# format!("{}: {}", self.username, self.content)
# }
# }
#
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
戻り値の型に impl Summary を使うことで、returns_summarizable 関数が、具体的な型名を挙げることなく、Summary トレイトを実装している何らかの型を返すことを指定できます。この場合、returns_summarizable は SocialPost を返しますが、この関数を呼び出すコードはそれを知る必要はありません。
戻り値の型を、それが実装しているトレイトだけで指定できることは、13章で扱うクロージャとイテレータの文脈で特に有用です。クロージャとイテレータは、コンパイラだけが知っている型や、指定するには非常に長い型を生み出します。impl Trait 構文を使うと、非常に長い型を書き出さなくても、関数が Iterator トレイトを実装する何らかの型を返すことを簡潔に指定できます。
しかし、impl Trait を使えるのは、単一の型を返す場合だけです。たとえば、戻り値の型を impl Summary として、NewsArticle または SocialPost のいずれかを返す次のコードは動作しません。
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 SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
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 {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
NewsArticle または SocialPost のいずれかを返すことが許されないのは、コンパイラにおける impl Trait 構文の実装方法に関する制約があるためです。このような振る舞いを持つ関数の書き方については、18章の「トレイトオブジェクトを使って共有された振る舞いを抽象化する」節で扱います。
トレイト境界を使って条件付きでメソッドを実装する
ジェネリック型パラメータを使う impl ブロックにトレイト境界を用いることで、指定したトレイトを実装している型に対してだけ、条件付きでメソッドを実装できます。たとえば、リスト10-15の型 Pair<T> は、常に new 関数を実装しており、新しい Pair<T> のインスタンスを返します(Self は impl ブロックの型に対する型エイリアスであり、この場合は Pair<T> であることを、5章の「メソッド記法」節で思い出してください)。しかし、次の impl ブロックでは、Pair<T> の内部の型 T が、比較を可能にする PartialOrd トレイト かつ 表示を可能にする Display トレイトを実装している場合にのみ、Pair<T> は cmp_display メソッドを実装します。
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);
}
}
}
また、別のトレイトを実装している任意の型に対して、トレイトを条件付きで実装することもできます。トレイト境界を満たすすべての型に対するトレイトの実装は、ブランケット実装 と呼ばれ、Rust標準ライブラリで広く使われています。たとえば、標準ライブラリは Display トレイトを実装している任意の型に対して ToString トレイトを実装しています。標準ライブラリの impl ブロックは、次のコードに似ています。
impl<T: Display> ToString for T {
// --中略--
}
標準ライブラリにはこのブランケット実装があるため、Display トレイトを実装している任意の型に対して、ToString トレイトで定義された to_string メソッドを呼び出せます。たとえば、整数は Display を実装しているので、次のように整数を対応する String 値に変換できます。
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
ブランケット実装は、そのトレイトのドキュメントの「Implementors」セクションに表示されます。
トレイトとトレイト境界を使うと、ジェネリック型パラメータを用いて重複を減らすコードを書けるだけでなく、そのジェネリック型に特定の振る舞いを持たせたいことをコンパイラに指定できます。するとコンパイラは、トレイト境界の情報を使って、私たちのコードとともに使われるすべての具体的な型が正しい振る舞いを提供しているかを検査できます。動的型付け言語では、ある型にそのメソッドが定義されていないのにそのメソッドを呼び出すと、実行時にエラーになります。しかし Rust では、この種のエラーはコンパイル時に移されるため、コードを実行できるようになる前に問題を修正することが強制されます。さらに、振る舞いを実行時にチェックするコードを書く必要はありません。なぜなら、すでにコンパイル時にチェックしているからです。これにより、ジェネリクスの柔軟性を犠牲にすることなく、性能が向上します。