rust-traits - 背景
歴史的経緯
ポリモーフィズムの進化
- Simula で導入
- 「is-a」関係
- 実装の継承 - Java で普及
- 実装と型の分離
- 多重継承の問題回避 - Haskell で導入
- アドホック多相
- 実装の後付け可能 - 型クラスに影響を受ける
- ゼロコスト抽象化
- コヒーレンス規則トレイトの設計原則
// トレイト = 型が持つべき振る舞いの定義
trait Greet {
fn greet(&self) -> String;
}
// 既存の型にも後から実装可能
impl Greet for String {
fn greet(&self) -> String {
format!("Hello, {}!", self)
}
}
コンピュータサイエンス的な意味
静的ディスパッチ(単相化)
fn print_twice<T: Display>(item: T) {
println!("{}", item);
println!("{}", item);
}
// コンパイル時に具体的な型ごとにコード生成
// print_twice::<i32>()
// print_twice::<String>()
動的ディスパッチ
fn print_any(item: &dyn Display) {
println!("{}", item);
}
// vtable を通じた間接呼び出し
// 実行時にメソッドを解決
比較
| 方式 |
利点 |
欠点 |
| 静的 |
高速、インライン化 |
バイナリサイズ増加 |
| 動的 |
柔軟、サイズ一定 |
vtableオーバーヘッド |
関連型 vs 型パラメータ
// 型パラメータ: 呼び出し側が型を決定
trait Container<T> {
fn get(&self) -> T;
}
// 関連型: 実装側が型を決定
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
実践での活用
拡張メソッド
// 標準型に機能を追加
trait StringExt {
fn truncate_at(&self, max_len: usize) -> &str;
}
impl StringExt for str {
fn truncate_at(&self, max_len: usize) -> &str {
if self.len() <= max_len {
self
} else {
&self[..max_len]
}
}
}
// 使用
"hello world".truncate_at(5); // "hello"
依存性の注入
trait Database {
fn query(&self, sql: &str) -> Vec<Row>;
}
struct UserService<D: Database> {
db: D,
}
// テスト時にモックを注入
struct MockDb;
impl Database for MockDb {
fn query(&self, _: &str) -> Vec<Row> { vec![] }
}
プラグインシステム
trait Plugin {
fn name(&self) -> &str;
fn execute(&self, input: &str) -> String;
}
struct PluginManager {
plugins: Vec<Box<dyn Plugin>>,
}
実世界とのギャップ
Orphan Rule(孤児規則)
// 外部のトレイトを外部の型に実装できない
// impl Display for Vec<T> { } // エラー!
// 解決策: ニュータイプパターン
struct MyVec<T>(Vec<T>);
impl<T: Display> Display for MyVec<T> {
// OK
}
オブジェクト安全性
trait NotObjectSafe {
fn new() -> Self; // Self を返すのでNG
fn generic<T>(&self, t: T); // ジェネリックメソッドはNG
}
// dyn NotObjectSafe は使用不可
トレイト境界の複雑さ
// 複雑な境界は where 句で整理
fn process<T, U, V>(a: T, b: U, c: V) -> String
where
T: Display + Clone + Send,
U: Debug + Default,
V: Iterator<Item = T>,
{
// ...
}