高度なトレイト
最初にトレイトについて講義したのは、第10章の「トレイト: 共通の振る舞いを定義する」節でしたが、 ライフタイム同様、より高度な詳細は議論しませんでした。今や、Rustに詳しくなったので、核心に迫れるでしょう。
関連型でトレイト定義においてプレースホルダーの型を指定する
関連型は、トレイトのメソッド定義がシグニチャでプレースホルダーの型を使用できるように、トレイトと型のプレースホルダーを結び付けます。 トレイトを実装するものがこの特定の実装で型の位置に使用される具体的な型を指定します。そうすることで、 なんらかの型を使用するトレイトをトレイトを実装するまでその型が一体なんであるかを知る必要なく定義できます。
この章のほとんどの高度な機能は、稀にしか必要にならないと解説しました。関連型はその中間にあります: 本の他の部分で説明される機能よりは使用されるのが稀ですが、この章で議論される他の多くの機能よりは頻繁に使用されます。
関連型があるトレイトの一例は、標準ライブラリが提供するIteratorトレイトです。その関連型はItemと名付けられ、
Iteratorトレイトを実装している型が走査している値の型の代役を務めます。第13章の「Iteratorトレイトとnextメソッド」節で、
Iteratorトレイトの定義は、リスト19-20に示したようなものであることに触れました。
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } }
リスト19-20: 関連型ItemがあるIteratorトレイトの定義
型Itemはプレースホルダー型でnextメソッドの定義は、型Option<Self::Item>の値を返すことを示しています。
Iteratorトレイトを実装するものは、Itemの具体的な型を指定し、nextメソッドは、
その具体的な型の値を含むOptionを返します。
関連型は、ジェネリクスにより扱う型を指定せずに関数を定義できるという点でジェネリクスに似た概念のように思える可能性があります。 では、何故関連型を使用するのでしょうか?
2つの概念の違いを第13章からCounter構造体にIteratorトレイトを実装する例で調査しましょう。
リスト13-21で、Item型はu32だと指定しました:
ファイル名: src/lib.rs
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
この記法は、ジェネリクスと比較可能に思えます。では、何故単純にリスト19-21のように、
Iteratorトレイトをジェネリクスで定義しないのでしょうか?
#![allow(unused)] fn main() { pub trait Iterator<T> { fn next(&mut self) -> Option<T>; } }
リスト19-21: ジェネリクスを使用した架空のIteratorトレイトの定義
差異は、リスト19-21のようにジェネリクスを使用すると、各実装で型を注釈しなければならないことです;
Iterator<String> for Counterや他のどんな型にも実装することができるので、
CounterのIteratorの実装が複数できるでしょう。換言すれば、トレイトにジェネリックな引数があると、
毎回ジェネリックな型引数の具体的な型を変更してある型に対して複数回実装できるということです。
Counterに対してnextメソッドを使用する際に、どのIteratorの実装を使用したいか型注釈をつけなければならないでしょう。
関連型なら、同じ型に対してトレイトを複数回実装できないので、型を注釈する必要はありません。
関連型を使用する定義があるリスト19-20では、Itemの型は1回しか選択できませんでした。
1つしかimpl Iterator for Counterがないからです。Counterにnextを呼び出す度に、
u32値のイテレータが欲しいと指定しなくてもよいわけです。
デフォルトのジェネリック型引数と演算子オーバーロード
ジェネリックな型引数を使用する際、ジェネリックな型に対して既定の具体的な型を指定できます。これにより、
既定の型が動くのなら、トレイトを実装する側が具体的な型を指定する必要を排除します。ジェネリックな型に既定の型を指定する記法は、
ジェネリックな型を宣言する際に<PlaceholderType=ConcreteType>です。
このテクニックが有用になる場面の好例が、演算子オーバーロードです。演算子オーバーロードとは、
特定の状況で演算子(+など)の振る舞いをカスタマイズすることです。
Rustでは、独自の演算子を作ったり、任意の演算子をオーバーロードすることはできません。しかし、
演算子に紐づいたトレイトを実装することでstd::opsに列挙された処理と対応するトレイトをオーバーロードできます。
例えば、リスト19-22で+演算子をオーバーロードして2つのPointインスタンスを足し合わせています。
Point構造体にAddトレイトを実装することでこれを行なっています。
ファイル名: src/main.rs
use std::ops::Add; #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 }); }
リスト19-22: Addトレイトを実装してPointインスタンス用に+演算子をオーバーロードする
addメソッドは2つのPointインスタンスのx値と2つのPointインスタンスのy値を足します。
Addトレイトには、addメソッドから返却される型を決定するOutputという関連型があります。
このコードの既定のジェネリック型は、Addトレイト内にあります。こちらがその定義です:
#![allow(unused)] fn main() { trait Add<RHS=Self> { type Output; fn add(self, rhs: RHS) -> Self::Output; } }
このコードは一般的に馴染みがあるはずです: 1つのメソッドと関連型が1つあるトレイトです。
新しい部分は、RHS=Selfです: この記法は、デフォルト型引数と呼ばれます。
RHSというジェネリックな型引数("right hand side": 右辺の省略形)が、addメソッドのrhs引数の型を定義しています。
Addトレイトを実装する際にRHSの具体的な型を指定しなければ、RHSの型は標準でSelfになり、
これはAddを実装している型になります。
PointにAddを実装する際、2つのPointインスタンスを足したかったので、RHSの規定を使用しました。
既定を使用するのではなく、RHSの型をカスタマイズしたくなるAddトレイトの実装例に目を向けましょう。
異なる単位で値を保持する構造体、MillimetersとMeters(それぞれミリメートルとメートル)が2つあります。
ミリメートルの値をメートルの値に足し、Addの実装に変換を正しくしてほしいです。
AddをRHSにMetersのあるMillimetersに実装することができます。リスト19-23のように:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::ops::Add; struct Millimeters(u32); struct Meters(u32); impl Add<Meters> for Millimeters { type Output = Millimeters; fn add(self, other: Meters) -> Millimeters { Millimeters(self.0 + (other.0 * 1000)) } } }
リスト19-23: MillimetersにAddトレイトを実装して、MetersにMillimetersを足す
MillimetersをMetersに足すため、Selfという既定を使う代わりにimpl Add<Meters>を指定して、
RHS型引数の値をセットしています。
主に2通りの方法でデフォルト型引数を使用します:
- 既存のコードを破壊せずに型を拡張する
- ほとんどのユーザは必要としない特定の場合でカスタマイズを可能にする
標準ライブラリのAddトレイトは、2番目の目的の例です: 通常、2つの似た型を足しますが、
Addトレイトはそれ以上にカスタマイズする能力を提供します。Addトレイト定義でデフォルト型引数を使用することは、
ほとんどの場合、追加の引数を指定しなくてもよいことを意味します。つまり、トレイトを使いやすくして、
ちょっとだけ実装の定型コードが必要なくなるのです。
最初の目的は2番目に似ていますが、逆です: 既存のトレイトに型引数を追加したいなら、既定を与えて、 既存の実装コードを破壊せずにトレイトの機能を拡張できるのです。
明確化のためのフルパス記法: 同じ名前のメソッドを呼ぶ
Rustにおいて、別のトレイトのメソッドと同じ名前のメソッドがトレイトにあったり、両方のトレイトを1つの型に実装することを妨げるものは何もありません。 トレイトのメソッドと同じ名前のメソッドを直接型に実装することも可能です。
同じ名前のメソッドを呼ぶ際、コンパイラにどれを使用したいのか教える必要があるでしょう。両方ともflyというメソッドがある2つのトレイト、
PilotとWizard(訳注: パイロットと魔法使い)を定義したリスト19-24のコードを考えてください。
それから両方のトレイトを既にflyというメソッドが実装されている型Human(訳注: 人間)に実装します。
各flyメソッドは異なることをします。
ファイル名: src/main.rs
#![allow(unused)] fn main() { trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { // キャプテンのお言葉 println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { // 上がれ! println!("Up!"); } } impl Human { fn fly(&self) { // *激しく腕を振る* println!("*waving arms furiously*"); } } }
リスト19-24: 2つのトレイトにflyがあるように定義され、Humanに実装されつつ、
flyメソッドはHumanに直接にも実装されている
Humanのインスタンスに対してflyを呼び出すと、コンパイラは型に直接実装されたメソッドを標準で呼び出します。
リスト19-25のようにですね:
ファイル名: src/main.rs
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
リスト19-25: Humanのインスタンスに対してflyを呼び出す
このコードを実行すると、*waving arms furiously*と出力され、コンパイラがHumanに直接実装されたflyメソッドを呼んでいることを示しています。
Pilotトレイトか、Wizardトレイトのflyメソッドを呼ぶためには、
より明示的な記法を使用して、どのflyメソッドを意図しているか指定する必要があります。
リスト19-26は、この記法をデモしています。
ファイル名: src/main.rs
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
リスト19-26: どのトレイトのflyメソッドを呼び出したいか指定する
メソッド名の前にトレイト名を指定すると、コンパイラにどのflyの実装を呼び出したいか明確化できます。
また、Human::fly(&person)と書くこともでき、リスト19-26で使用したperson.fly()と等価ですが、
こちらの方は明確化する必要がないなら、ちょっと記述量が増えます。
このコードを実行すると、こんな出力がされます:
This is your captain speaking.
Up!
*waving arms furiously*
flyメソッドはself引数を取るので、1つのトレイトを両方実装する型が2つあれば、
コンパイラには、selfの型に基づいてどのトレイトの実装を使うべきかわかるでしょう。
しかしながら、トレイトの一部になる関連関数にはself引数がありません。同じスコープの2つの型がそのトレイトを実装する場合、
フルパス記法(fully qualified syntax)を使用しない限り、どの型を意図しているかコンパイラは推論できません。例えば、
リスト19-27のAnimalトレイトには、関連関数baby_name、構造体DogのAnimalの実装、
Dogに直接定義された関連関数baby_nameがあります。
ファイル名: src/main.rs
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { // スポット(Wikipediaによると、飼い主の事故死後もその人の帰りを待つ忠犬の名前の模様) String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { // 子犬 String::from("puppy") } } fn main() { // 赤ちゃん犬は{}と呼ばれる println!("A baby dog is called a {}", Dog::baby_name()); }
リスト19-27: 関連関数のあるトレイトとそのトレイトも実装し、同じ名前の関連関数がある型
このコードは、全ての子犬をスポットと名付けたいアニマル・シェルター(訳注: 身寄りのないペットを保護する保健所みたいなところ)用で、
Dogに定義されたbaby_name関連関数で実装されています。Dog型は、トレイトAnimalも実装し、
このトレイトは全ての動物が持つ特徴を記述します。赤ちゃん犬は子犬と呼ばれ、
それがDogのAnimalトレイトの実装のAnimalトレイトと紐づいたbase_name関数で表現されています。
mainで、Dog::baby_name関数を呼び出し、直接Dogに定義された関連関数を呼び出しています。
このコードは以下のような出力をします:
A baby dog is called a Spot
この出力は、欲しかったものではありません。Dogに実装したAnimalトレイトの一部のbaby_name関数を呼び出したいので、
コードはA baby dog is called a puppyと出力します。リスト19-26で使用したトレイト名を指定するテクニックは、
ここでは役に立ちません; mainをリスト19-28のようなコードに変更したら、コンパイルエラーになるでしょう。
ファイル名: src/main.rs
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
リスト19-28: Animalトレイトのbaby_name関数を呼び出そうとするも、コンパイラにはどの実装を使うべきかわからない
Animal::baby_nameはメソッドではなく関連関数であり、故にself引数がないので、どのAnimal::baby_nameが欲しいのか、
コンパイラには推論できません。こんなコンパイルエラーが出るでしょう:
error[E0283]: type annotations required: cannot resolve `_: Animal`
(エラー: 型注釈が必要です: `_: Animal`を解決できません)
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^
|
= note: required by `Animal::baby_name`
(注釈: `Animal::baby_name`に必要です)
Dogに対してAnimal実装を使用したいと明確化し、コンパイラに指示するには、フルパス記法を使う必要があります。
リスト19-29は、フルパス記法を使用する方法をデモしています。
ファイル名: src/main.rs
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
リスト19-29: フルパス記法を使ってDogに実装されているように、
Animalトレイトからのbaby_name関数を呼び出したいと指定する
コンパイラに山カッコ内で型注釈を提供し、これは、この関数呼び出しではDog型をAnimalとして扱いたいと宣言することで、
Dogに実装されたように、Animalトレイトのbaby_nameメソッドを呼び出したいと示唆しています。
もうこのコードは、望み通りの出力をします:
A baby dog is called a puppy
一般的に、フルパス記法は、以下のように定義されています:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
関連関数では、receiverがないでしょう: 他の引数のリストがあるだけでしょう。関数やメソッドを呼び出す箇所全部で、
フルパス記法を使用することもできるでしょうが、プログラムの他の情報からコンパイラが推論できるこの記法のどの部分も省略することが許容されています。
同じ名前を使用する実装が複数あり、どの実装を呼び出したいかコンパイラが特定するのに助けが必要な場合だけにこのより冗長な記法を使用する必要があるのです。
スーパートレイトを使用して別のトレイト内で、あるトレイトの機能を必要とする
時として、あるトレイトに別のトレイトの機能を使用させる必要がある可能性があります。この場合、 依存するトレイトも実装されることを信用する必要があります。信用するトレイトは、実装しているトレイトのスーパートレイトです。
例えば、アスタリスクをフレームにする値を出力するoutline_printメソッドがあるOutlinePrintトレイトを作りたくなったとしましょう。
つまり、Displayを実装し、(x, y)という結果になるPoint構造体が与えられて、
xが1、yが3のPointインスタンスに対してoutline_printを呼び出すと、以下のような出力をするはずです:
**********
* *
* (1, 3) *
* *
**********
outline_printの実装では、Displayトレイトの機能を使用したいです。故に、Displayも実装する型に対してだけOutlinePrintが動くと指定し、
OutlinePrintが必要とする機能を提供する必要があるわけです。トレイト定義でOutlinePrint: Displayと指定することで、
そうすることができます。このテクニックは、トレイトにトレイト境界を追加することに似ています。
リスト19-30は、OutlinePrintトレイトの実装を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {} *", output); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } }
リスト19-30: Displayからの機能を必要とするOutlinePrintトレイトを実装する
OutlinePrintはDisplayトレイトを必要とすると指定したので、Displayを実装するどんな型にも自動的に実装されるto_string関数を使えます。
トレイト名の後にコロンとDisplayトレイトを追加せずにto_stringを使おうとしたら、
現在のスコープで型&Selfにto_stringというメソッドは存在しないというエラーが出るでしょう。
Displayを実装しない型、Point構造体などにOutlinePrintを実装しようとしたら、何が起きるか確認しましょう:
ファイル名: src/main.rs
#![allow(unused)] fn main() { trait OutlinePrint {} struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} }
Displayが必要だけれども、実装されていないというエラーが出ます:
error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
これを修正するために、PointにDisplayを実装し、OutlinePrintが必要とする制限を満たします。
こんな感じで:
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } }
そうすれば、PointにOutlinePrintトレイトを実装してもコンパイルは成功し、
Pointインスタンスに対してoutline_printを呼び出し、アスタリスクのふちの中に表示することができます。
ニュータイプパターンを使用して外部の型に外部のトレイトを実装する
第10章の「型にトレイトを実装する」節で、トレイトか型がクレートにローカルな限り、型にトレイトを実装できると述べるオーファンルールについて触れました。 ニュータイプパターンを使用してこの制限を回避することができ、タプル構造体に新しい型を作成することになります。 (タプル構造体については、第5章の「異なる型を生成する名前付きフィールドのないタプル構造体を使用する」節で講義しました。) タプル構造体は1つのフィールドを持ち、トレイトを実装したい型の薄いラッパになるでしょう。そして、 ラッパの型はクレートにローカルなので、トレイトをラッパに実装できます。ニュータイプという用語は、 Haskellプログラミング言語に端を発しています。このパターンを使用するのに実行時のパフォーマンスを犠牲にすることはなく、 ラッパ型はコンパイル時に省かれます。
例として、Vec<T>にDisplayを実装したいとしましょう。DisplayトレイトもVec<T>型もクレートの外で定義されているので、
直接それを行うことはオーファンルールにより妨げられます。Vec<T>のインスタンスを保持するWrapper構造体を作成できます;
そして、WrapperにDisplayを実装し、Vec<T>値を使用できます。リスト19-31のように。
ファイル名: src/main.rs
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {}", w); }
リスト19-31: Vec<String>の周りにWrapperを作成してDisplayを実装する
Displayの実装は、self.0で中身のVec<T>にアクセスしています。Wrapperはタプル構造体で、
Vec<T>がタプルの添え字0の要素だからです。それから、Wrapperに対してDisplay型の機能を使用できます。
このテクニックを使用する欠点は、Wrapperが新しい型なので、保持している値のメソッドがないことです。
self.0に委譲して、WrapperをVec<T>と全く同様に扱えるように、Wrapperに直接Vec<T>の全てのメソッドを実装しなければならないでしょう。
内部の型が持つ全てのメソッドを新しい型に持たせたいなら、
Derefトレイト(第15章の「Derefトレイトでスマートポインタを普通の参照のように扱う」節で議論しました)をWrapperに実装して、
内部の型を返すことは解決策の1つでしょう。内部の型のメソッド全部をWrapper型に持たせたくない(例えば、Wrapper型の機能を制限するなど)なら、
本当に欲しいメソッドだけを手動で実装しなければならないでしょう。
もう、トレイトに関してニュータイプパターンが使用される方法を知りました; トレイトが関連しなくても、 有用なパターンでもあります。焦点を変更して、Rustの型システムと相互作用する一部の高度な方法を見ましょう。