高度なトレイト
最初にトレイトについて講義したのは、第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>; } }
型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<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 }); }
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)) } } }
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*"); } } }
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(); }
このコードを実行すると、*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(); }
メソッド名の前にトレイト名を指定すると、コンパイラにどの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()); }
このコードは、全ての子犬をスポットと名付けたいアニマル・シェルター(訳注
: 身寄りのないペットを保護する保健所みたいなところ)用で、
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());
}
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()); }
コンパイラに山カッコ内で型注釈を提供し、これは、この関数呼び出しでは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)); } } }
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); }
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の型システムと相互作用する一部の高度な方法を見ましょう。