概要
Rustのトレイトについて勉強したのでメモ.
読んだもの
- 4.19 トレイト - プログラミング言語Rust 第1版
- 4.22 トレイトオブジェクト - プログラミング言語Rust 第1版
- 19.1 トレイトオブジェクトで異なる型の値を許可する - プログラミング言語Rust 第2版
- 4.31 サイズ不定型 - プログラミング言語Rust 第1版
- RustのSizedとfatポインタ - 簡潔なQ
- 安定化間近!Rustのimpl Traitを今こそ理解する - 簡潔なQ
- RFC2113
トレイト
- ポリモーフィズムの一種で, 部分型多相を実現するもの
- 複数の型に共通する振舞いを抽象化できる
- よくある間違い
- 🙅 共通のデータ構造に対する抽象化をすることが目的ではない
- 🙆 共通の振舞いに対する抽象化をすることが目的
- トレイトでは振舞いを定義できるが, 抽象フィールドに相当するものが定義できない
- トレイトの命名も「どんなデータ構造を持つのか」ではなく「どんな振舞いを持つのか」を的確に表したもののほうが好ましい?
Figure
よりもHasArea
のほうが好ましい?
struct
ブロックにデータ構造を記述し,impl
ブロックに振舞いの実装を記述するという考え方
- 🙅 変数の型や配列の要素の型, 型パラメータ, 引数の型, 戻り値の型などにトレイトを指定できない
- 🙆 主にトレイト境界に指定できる
- 詳しくは「トレイトオブジェクト」にて説明
- 🙅 共通のデータ構造に対する抽象化をすることが目的ではない
- サンプルコード: https://play.rust-lang.org/?gist=a1c734a7db83efa1976e96af37b4b5a9&version=stable&mode=debug
トレイト境界
- コンパイラはそのトレイトを実装する任意の型がそこに現れることを保証する
- サンプルコード: https://play.rust-lang.org/?gist=2c6e47b4bab369bd6036e12584f423b5&version=stable&mode=debug
トレイトの制限
- Rustでは変数の型や配列の要素の型, 型パラメータ, 引数の型, 戻り値の型などにトレイトを指定することができない
- 🙅
let val: HasArea = Circle {}
- 🙅
let array: [HasArea; 2] = [Circle {}, Rectangle {}]
- 🙅
let vec: Vec<HasArea> = vec![Circle {}, Rectangle {}]
- 🙅
fn get_figure() -> HasArea { Circle {} }
- これらがコンパイルエラーとなるのは, 変数宣言や引数の型, 型パラメータ, 戻り値の型に
Sized
トレイトを実装する型が要求されているため- 参考: RustのSizedとfatポインタ - 簡潔なQ
- 型のサイズが分かっていないとコンパイラがデータを格納する領域の大きさを確定できないので当然といえば当然
- トレイトが指定できるのはトレイト境界を要求するところ (ジェネリック関数/ジェネリック構造体)
- 🙆
fn calc_area<T: HasArea>(figure: T) -> f64 { figure.area() }
- 🙆
struct Canvas<T: HasArea> { figures: Vec<T> }
- 参考: トレイト
- 🙆
- トレイト境界にトレイトを使用したジェネリック関数やジェネリック構造体は静的ディスパッチにより特殊化される
- 参考: トレイトオブジェクト
- 特殊化されることで型のサイズが確定する
- 🙅
- では,
Vec<HasArea>
などを実現したい場合はどうすれば良いか-
enum
を使う- サンプルコード: https://play.rust-lang.org/?gist=1e497a09e2b01a56391ae5439e4d6c56&version=stable&mode=debug
- 問題点
- 共通の振舞いを定義できない
match
式などを利用しないと中身を取り出せないenum
のヴァリアントを後から追加することができない (拡張性がない)
- トレイトオブジェクトを使う
- サンプルコード
- 問題点
- トレイトオブジェクトは仮想関数テーブルを用いた動的ディスパッチが行われるので実行時にコストがかかる
-
トレイトオブジェクト
- 動的ディスパッチを実現する機能
- fat pointerの一種
std::raw
モジュールにトレイトオブジェクトを表す構造体が定義されている- 参考: raw.rs.html -- source
TraitObject
は暗黙にSized
を実装しているので変数の型などに指定できる- x86-64環境では16byte
Box<T>
or&T
or&mut T
を使うことでコンパイラによりトレイトオブジェクトとして特別扱いされるBox<T>
と&T
の違いBox<T>
はヒープ上にあるT型のデータへのポインタ (pointer) を持つデータを,&T
は (ヒープ上やスタック上などのあらゆる場所にある) T型のデータへの参照 (reference) を表す- 注:
&Box<T>
は参照だが,Box<T>
は参照ではない
- 注:
&T
は参照なので, Rustの所有権システムに関する借用のルールが適用される
- 仮想関数テーブルを用いた動的ディスパッチが行われる
- 参考: トレイトオブジェクト
- 例外的に本来使用できないところにトレイトを使用できる場合もある
- 例:
Box<T>
のT
は型パラメータであるがトレイトを指定できる- この理由は
Box
構造体の実装を見ると分かる - 参考: https://doc.rust-lang.org/1.17.0/src/alloc/boxed.rs.html#108
- トレイト境界が
T: ?Sized
となっており,T
型がSized
トレイトを実装していることを要求していない - よって
T
にトレイトを指定できる
- この理由は
- トレイトオブジェクトにできるトレイトの条件は「そのトレイトがオブジェクト安全であること」
- 詳細は「オブジェクト安全」にて
- 参考: トレイトオブジェクト
- サンプルコード: https://play.rust-lang.org/?gist=057fb336f09229c8cad5b86e5c9aa665&version=stable&mode=debug
オブジェクト安全
- トレイトがオブジェクト安全であることの条件
- トレイトが
Self: Sized
を要求しないこと- トレイトが取りうるサイズが不定という条件に違反する
- そもそもトレイトは
Sized
を実装していないので当然といえば当然
- トレイトのメソッド全てがオブジェクト安全であること
- トレイトが
- メソッドがオブジェクト安全であることの条件
- どのような型パラメータも持ってはならない
- 型パラメータを持つとそのメソッドを特殊化しなければならないが, トレイトオブジェクトに対して特殊化することは難しそうだし, 納得できる
-
Self
を使ってはならない
- どのような型パラメータも持ってはならない
- 参考: トレイトオブジェクト
dyn Trait
- ある型がトレイトオブジェクトであることを明示するために導入された新記法
Box<Sized>
とBox<Trait>
でどちらがトレイトオブジェクトでそうでないかひと目で区別できないという問題を解決する- 例:
Box<Trait>
をBox<dyn Trait>
に置き換える
- 例:
- まだ安定化されていない機能
- 参考: RFC2113
- 対応表
従来記法 | 新記法 |
---|---|
Box<Trait> |
Box<dyn Trait> |
&Trait |
&dyn Trait |
&mut Trait |
&mut dyn Trait |
impl Trait
- トレイトで静的ディスパッチを行うために導入された新機能
- 勿論上記で述べたように型のサイズが確定しないと静的ディスパッチは行えないので, 制約が多い機能となる
- サイズが確定するところまではコンパイラが頑張って静的ディスパッチしてくれる
- 参考: 安定化間近!Rustのimpl Traitを今こそ理解する - 簡潔なQ
- サンプルコード: https://play.rust-lang.org/?gist=d2eaa515a10bbdd46300b41a569ace74&version=stable&mode=debug