トレイトオブジェクトで異なる型の値を許容する
第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)] fn main() { pub trait Draw { fn draw(&self); } }
リスト17-3: Drawトレイトの定義
この記法は、第10章のトレイトの定義方法に関する議論で馴染み深いはずです。その次は、新しい記法です:
リスト17-4では、componentsというベクタを保持するScreenという名前の構造体を定義しています。
このベクタの型はBox<Draw>で、これはトレイトオブジェクトです; Drawトレイトを実装するBox内部の任意の型に対する代役です。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Draw { fn draw(&self); } pub struct Screen { pub components: Vec<Box<Draw>>, } }
リスト17-4: Drawトレイトを実装するトレイトオブジェクトのベクタを保持するcomponentsフィールドがある
Screen構造体の定義
Screen構造体に、componentsの各要素に対してdrawメソッドを呼び出すrunというメソッドを定義します。
リスト17-5のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] 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(); } } } }
リスト17-5: 各コンポーネントに対してdrawメソッドを呼び出すScreenのrunメソッド
これは、トレイト境界を伴うジェネリックな型引数を使用する構造体を定義するのとは異なる動作をします。
ジェネリックな型引数は、一度に1つの具体型にしか置き換えられないのに対して、トレイトオブジェクトは、
実行時にトレイトオブジェクトに対して複数の具体型で埋めることができます。例として、
ジェネリックな型とトレイト境界を使用してリスト17-6のようにScreen構造体を定義することもできました:
ファイル名: src/lib.rs
#![allow(unused)] 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(); } } } }
リスト17-6: ジェネリクスとトレイト境界を使用したScreen構造体とrunメソッドの対立的な実装
こうすると、全てのコンポーネントの型が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)] 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 // 実際にボタンを描画するコード } } }
リスト17-7: Drawトレイトを実装するある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
//セレクトボックスを実際に描画するコード
}
}
リスト17-8: guiを使用し、SelectBox構造体にDrawトレイトを実装する別のクレート
ライブラリの使用者はもう、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();
}
リスト17-9: トレイトオブジェクトを使って同じトレイトを実装する異なる型の値を格納する
ライブラリを記述した時点では、誰かが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();
}
リスト17-10: トレイトオブジェクトのトレイトを実装しない型の使用を試みる
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)] 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を参照されたし。