トレイトオブジェクトで異なる型の値を許容する
第8章で、ベクタの1つの制限は、たった1つの型の要素を保持することしかできないことだと述べました。
リスト8-10で整数、浮動小数点数、テキストを保持する列挙子のあるSpreadsheetCell
enumを定義して、
これを回避しました。つまり、各セルに異なる型のデータを格納しつつ、1行のセルを表すベクタを保持するということです。
コンパイル時にわかるある固定されたセットの型にしか取り替え可能な要素がならない場合には、
完璧な解決策です。
ところが、時として、ライブラリの使用者が特定の場面で合法になる型のセットを拡張できるようにしたくなることがあります。
これをどう実現する可能性があるか示すために、各アイテムにdraw
メソッドを呼び出してスクリーンに描画するという、
GUIツールで一般的なテクニックをしてあるリストの要素を走査する例のGUIツールを作ります。
GUIライブラリの構造を含むgui
と呼ばれるライブラリクレートを作成します。
このクレートには、他人が使用できるButton
やTextField
などの型が包含されるかもしれません。
さらに、gui
の使用者は、描画可能な独自の型を作成したくなるでしょう: 例えば、
ある人はImage
を追加し、別の人はSelectBox
を追加するかもしれません。
この例のために本格的なGUIライブラリは実装するつもりはありませんが、部品がどう組み合わさるかは示します。
ライブラリの記述時点では、他のプログラマが作成したくなる可能性のある型全てを知る由もなければ、定義することもできません。
しかし、gui
は異なる型の多くの値を追いかけ、この異なる型の値に対してdraw
メソッドを呼び出す必要があることは、
確かにわかっています。draw
メソッドを呼び出した時に正確に何が起きるかを知っている必要はありません。
値にそのメソッドが呼び出せるようあることだけわかっていればいいのです。
継承のある言語でこれを行うには、draw
という名前のメソッドがあるComponent
というクラスを定義するかもしれません。
Button
、Image
、SelectBox
などの他のクラスは、Component
を継承し、故にdraw
メソッドを継承します。
個々にdraw
メソッドをオーバーライドして、独自の振る舞いを定義するものの、フレームワークは、
Component
インスタンスであるかのようにその型全部を扱い、この型に対してdraw
を呼び出します。
ですが、Rustに継承は存在しないので、使用者に新しい型で拡張してもらうためにgui
ライブラリを構成する他の方法が必要です。
一般的な振る舞いにトレイトを定義する
gui
に欲しい振る舞いを実装するには、draw
という1つのメソッドを持つDraw
というトレイトを定義します。
それからトレイトオブジェクトを取るベクタを定義できます。トレイトオブジェクトは、
指定したトレイトを実装するある型のインスタンスを指します。&
参照やBox<T>
スマートポインタなどの、
何らかのポインタを指定し、それから関係のあるトレイトを指定する(トレイトオブジェクトがポインタを使用しなければならない理由については、
第19章の「動的サイズ付け型とSizedトレイト」節で語ります)ことでトレイトオブジェクトを作成します。
ジェネリックまたは具体的な型があるところにトレイトオブジェクトは使用できます。どこでトレイトオブジェクトを使用しようと、
Rustの型システムは、コンパイル時にその文脈で使用されているあらゆる値がそのトレイトオブジェクトのトレイトを実装していることを保証します。
結果としてコンパイル時に可能性のある型を全て知る必要はなくなるのです。
Rustでは、構造体とenumを他の言語のオブジェクトと区別するために「オブジェクト」と呼ぶことを避けていることに触れましたね。
構造体やenumにおいて、構造体のフィールドのデータやimpl
ブロックの振る舞いは区分けされているものの、
他の言語では1つの概念に押し込められるデータと振る舞いは、しばしばオブジェクトと分類されます。
しかしながら、トレイトオブジェクトは、データと振る舞いをごちゃ混ぜにするという観点で他の言語のオブジェクトに近いです。
しかし、トレイトオブジェクトは、データを追加できないという点で伝統的なオブジェクトと異なっています。
トレイトオブジェクトは、他の言語のオブジェクトほど一般的に有用ではありません:
その特定の目的は、共通の振る舞いに対して抽象化を行うことです。
リスト17-3は、draw
という1つのメソッドを持つDraw
というトレイトを定義する方法を示しています:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { pub trait Draw { fn draw(&self); } #}
この記法は、第10章のトレイトの定義方法に関する議論で馴染み深いはずです。その次は、新しい記法です:
リスト17-4では、components
というベクタを保持するScreen
という名前の構造体を定義しています。
このベクタの型はBox<Draw>
で、これはトレイトオブジェクトです; Draw
トレイトを実装するBox
内部の任意の型に対する代役です。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # pub trait Draw { # fn draw(&self); # } # pub struct Screen { pub components: Vec<Box<Draw>>, } #}
Screen
構造体に、components
の各要素に対してdraw
メソッドを呼び出すrun
というメソッドを定義します。
リスト17-5のようにですね:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # pub trait Draw { # fn draw(&self); # } # # pub struct Screen { # pub components: Vec<Box<Draw>>, # } # impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } #}
これは、トレイト境界を伴うジェネリックな型引数を使用する構造体を定義するのとは異なる動作をします。
ジェネリックな型引数は、一度に1つの具体型にしか置き換えられないのに対して、トレイトオブジェクトは、
実行時にトレイトオブジェクトに対して複数の具体型で埋めることができます。例として、
ジェネリックな型とトレイト境界を使用してリスト17-6のようにScreen
構造体を定義することもできました:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # pub trait Draw { # fn draw(&self); # } # pub struct Screen<T: Draw> { pub components: Vec<T>, } impl<T> Screen<T> where T: Draw { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } #}
こうすると、全てのコンポーネントの型がButton
だったり、TextField
だったりするScreen
のインスタンスに制限されてしまいます。
絶対に同種のコレクションしか持つ予定がないのなら、ジェネリクスとトレイト境界は、
定義がコンパイル時に具体的な型を使用するように単相化されるので、望ましいです。
一方で、メソッドがトレイトオブジェクトを使用すると、1つのScreen
インスタンスが、
Box<Button>
とBox<TextField>
を含むVec<T>
を保持できます。
この動作方法を見、それから実行時性能の裏の意味について語りましょう。
トレイトを実装する
さて、Draw
トレイトを実装する型を追加しましょう。Button
型を提供します。ここも、実際にGUIライブラリを実装することは、
この本の範疇を超えているので、draw
メソッドの本体は、何も有用な実装はしません。実装がどんな感じになるか想像するために、
Button
構造体は、width
、height
、label
フィールドを持っている可能性があります。
リスト17-7に示したようにですね:
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { # pub trait Draw { # fn draw(&self); # } # pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // code to actually draw a button // 実際にボタンを描画するコード } } #}
Button
のwidth
、height
、label
フィールドは、TextField
型のように、
それらのフィールドプラスplaceholder
フィールドを代わりに持つ可能性のある他のコンポーネントのフィールドとは異なるでしょう。
スクリーンに描画したい型のコンポーネントはそれぞれDraw
トレイトを実装しますが、
Button
がここでしているように、draw
メソッドでは異なるコードを使用してその特定の型を描画する方法を定義しています(実際のGUIコードは、
この章の範疇を超えるのでありませんが)。例えば、Button
には、ユーザがボタンをクリックした時に起こることに関連するメソッドを含む、
追加のimpl
ブロックがある可能性があります。この種のメソッドは、TextField
のような型には適用されません。
ライブラリの使用者が、width
、height
、options
フィールドのあるSelectBox
構造体を実装しようと決めたら、
SelectBox
型にもDraw
トレイトを実装します。リスト17-8のようにですね:
ファイル名: src/main.rs
extern crate gui;
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
//セレクトボックスを実際に描画するコード
}
}
ライブラリの使用者はもう、main
関数を書き、Screen
インスタンスを生成できます。Screen
インスタンスには、
それぞれをBox<T>
に放り込んでトレイトオブジェクト化してSelectBox
とButton
を追加できます。
それからScreen
インスタンスに対してrun
メソッドを呼び出すことができ、そうすると各コンポーネントのdraw
が呼び出されます。
リスト17-9は、この実装を示しています:
ファイル名: src/main.rs
use gui::{Screen, Button};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
// はい
String::from("Yes"),
// 多分
String::from("Maybe"),
// いいえ
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
// 了解
label: String::from("OK"),
}),
],
};
screen.run();
}
ライブラリを記述した時点では、誰かがSelectBox
型を追加する可能性があるなんて知りませんでしたが、
Screen
の実装は、新しい型を処理し、描画することができました。何故なら、SelectBox
はDraw
型、
つまり、draw
メソッドを実装しているからです。
この値の具体的な型ではなく、値が応答したメッセージにのみ関係するという概念は、
動的型付け言語のダックタイピングに似た概念です: アヒルのように歩き、鳴くならば、
アヒルに違いないのです!リスト17-5のScreen
のrun
の実装では、run
は、
各コンポーネントの実際の型がなんであるか知る必要はありません。コンポーネントが、
Button
やSelectBox
のインスタンスであるかを確認することはなく、コンポーネントのdraw
メソッドを呼び出すだけです。
components
ベクタでBox<Draw>
を値の型として指定することで、Screen
を、
draw
メソッドを呼び出せる値を必要とするように定義できたのです。
注釈: ダックタイピングについて
ご存知かもしれませんが、ダックタイピングについて補足です。ダックタイピングとは、動的型付け言語やC++のテンプレートで使用される、 特定のフィールドやメソッドがあることを想定してコンパイルを行い、実行時に実際にあることを確かめるというプログラミング手法です。 ダック・テストという思考法に由来するそうです。
ダックタイピングの利点は、XMLやJSONなど、厳密なスキーマがないことが多い形式を扱いやすくなること、 欠点は、実行してみるまで動くかどうかわからないことでしょう。
トレイトオブジェクトとRustの型システムを使用してダックタイピングを活用したコードに似たコードを書くことの利点は、 実行時に値が特定のメソッドを実装しているか確認したり、値がメソッドを実装していない時にエラーになることを心配したりする必要は絶対になく、 とにかく呼び出せることです。コンパイラは、値が、トレイトオブジェクトが必要としているトレイトを実装していなければ、 コンパイルを通さないのです。
例えば、リスト17-10は、コンポーネントにString
のあるScreen
を作成しようとした時に起こることを示しています:
ファイル名: src/main.rs
extern crate gui;
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![
Box::new(String::from("Hi")),
],
};
screen.run();
}
String
はDraw
トレイトを実装していないので、このようなエラーが出ます:
error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied
--> src/main.rs:7:13
|
7 | Box::new(String::from("Hi")),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait gui::Draw is not
implemented for `std::string::String`
|
= note: required for the cast to the object type `gui::Draw`
このエラーは、渡すことを意図していないものをScreen
に渡しているので、異なる型を渡すべきか、
Screen
がdraw
を呼び出せるようにString
にDraw
を実装するべきのどちらかであることを知らせてくれています。
トレイトオブジェクトは、ダイナミックディスパッチを行う
第10章の「ジェネリクスを使用したコードのパフォーマンス」節でジェネリクスに対してトレイト境界を使用した時に、 コンパイラが行う単相化過程の議論を思い出してください: コンパイラは、関数やメソッドのジェネリックでない実装を、 ジェネリックな型引数の箇所に使用している具体的な型に対して生成するのでした。単相化の結果吐かれるコードは、 スタティックディスパッチを行い、これは、コンパイル時にコンパイラがどのメソッドを呼び出しているかわかる時のことです。 これは、ダイナミックディスパッチとは対照的で、この時、コンパイラは、コンパイル時にどのメソッドを呼び出しているのかわかりません。 ダイナミックディスパッチの場合、コンパイラは、実行時にどのメソッドを呼び出すか弾き出すコードを生成します。
トレイトオブジェクトを使用すると、コンパイラはダイナミックディスパッチを使用しなければなりません。 コンパイラは、トレイトオブジェクトを使用しているコードで使用される可能性のある型全てを把握しないので、 どの型に実装されたどのメソッドを呼び出すかわからないのです。代わりに実行時に、トレイトオブジェクト内でポインタを使用して、 コンパイラは、どのメソッドを呼ぶか知ります。スタティックディスパッチでは行われないこの検索が起きる時には、 実行時コストがあります。また、ダイナミックディスパッチは、コンパイラがメソッドのコードをインライン化することも妨げ、 そのため、ある種の最適化が不可能になります。ですが、リスト17-5で記述し、 リスト17-9ではサポートできたコードで追加の柔軟性を確かに得られたので、考慮すべき代償です。
トレイトオブジェクトには、オブジェクト安全性が必要
トレイトオブジェクトには、オブジェクト安全なトレイトしか作成できません。 トレイトオブジェクトを安全にする特性全てを司る複雑な規則がありますが、実際には、2つの規則だけが関係があります。 トレイトは、トレイト内で定義されているメソッド全てに以下の特性があれば、オブジェクト安全になります。
- 戻り値の型が
Self
でない。 - ジェネリックな型引数がない。
Self
キーワードは、トレイトやメソッドを実装しようとしている型の別名です。トレイトオブジェクトは、
一旦、トレイトオブジェクトを使用したら、コンパイラにはそのトレイトを実装している具体的な型を知りようがないので、
オブジェクト安全でなければなりません。トレイトメソッドが具体的なSelf
型を返すのに、
トレイトオブジェクトがSelf
の具体的な型を忘れてしまったら、メソッドが元の具体的な型を使用できる手段はなくなってしまいます。
同じことがトレイトを使用する時に具体的な型引数で埋められるジェネリックな型引数に対しても言えます:
具体的な型がトレイトを実装する型の一部になるのです。トレイトオブジェクトの使用を通して型が忘却されたら、
そのジェネリックな型引数を埋める型がなんなのか知る術はないのです。
メソッドがオブジェクト安全でないトレイトの例は、標準ライブラリのClone
トレイトです。
Clone
トレイトのclone
メソッドのシグニチャは以下のような感じです:
# #![allow(unused_variables)] #fn main() { pub trait Clone { fn clone(&self) -> Self; } #}
String
型はClone
トレイトを実装していて、String
のインスタンスに対してclone
メソッドを呼び出すと、
String
のインスタンスが返ってきます。同様に、Vec<T>
のインスタンスに対してclone
を呼び出すと、
Vec<T>
のインスタンスが返ってきます。clone
のシグニチャは、Self
の代わりに入る型を知る必要があります。
それが、戻り値の型になるからです。
コンパイラは、トレイトオブジェクトに関していつオブジェクト安全の規則を侵害するようなことを試みているかを示唆します。
例えば、リスト17-4でScreen
構造体を実装してDraw
トレイトではなく、
Clone
トレイトを実装した型を保持しようとしたとしましょう。こんな感じで:
pub struct Screen {
pub components: Vec<Box<Clone>>,
}
こんなエラーになるでしょう:
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
(エラー: `std::clone::Clone`トレイトは、オブジェクトにすることはできません)
--> src/lib.rs:2:5
|
2 | pub components: Vec<Box<Clone>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
made into an object
|
= note: the trait cannot require that `Self : Sized`
(注釈: このトレイトは、`Self : Sized`を満たせません)
このエラーは、このようにこのトレイトをトレイトオブジェクトとして使用することはできないことを意味しています。 オブジェクト安全性についての詳細に興味があるのなら、Rust RFC 255を参照されたし。