rust-traits - 解説
実装の詳細
トレイトの基本構造
// トレイト定義
trait MyTrait {
// 必須メソッド(実装者が定義)
fn required(&self) -> i32;
// デフォルト実装(オーバーライド可能)
fn with_default(&self) -> i32 {
self.required() * 2
}
// 関連定数
const MAX: i32 = 100;
// 関連型
type Output;
}
実装の分類
// 1. 自分の型に自分のトレイト
struct MyType;
trait MyTrait {}
impl MyTrait for MyType {} // OK
// 2. 自分の型に外部のトレイト
impl std::fmt::Display for MyType {} // OK
// 3. 外部の型に自分のトレイト
impl MyTrait for String {} // OK
// 4. 外部の型に外部のトレイト
impl std::fmt::Display for Vec<i32> {} // NG (孤児規則)
よくある間違い
1. Self の使用位置
trait Factory {
// Self はトレイトオブジェクトで使用不可
fn create() -> Self; // オブジェクト安全でない
// 代替: Box を返す
fn create_boxed() -> Box<dyn Factory>
where
Self: Sized;
}
2. ジェネリックメソッド
trait Process {
// ジェネリックメソッドはオブジェクト安全でない
fn process<T>(&self, item: T); // NG for dyn
// 代替: 具体的な型または関連型
fn process_str(&self, item: &str);
}
3. 境界の過不足
// 不足: コンパイルエラー
fn print<T>(item: T) {
println!("{}", item); // T: Display が必要
}
// 過剰: 不要な制約
fn store<T: Display + Debug + Clone + Send + Sync>(item: T) {
let _ = item; // 何も使っていない
}
// 適切: 必要最小限
fn print<T: Display>(item: T) {
println!("{}", item);
}
トレイトオブジェクト
vtable の仕組み
┌─────────────────┐
│ dyn Trait │
├─────────────────┤
│ data: *const T │ → 実際のデータ
│ vtable: *const │ → vtable
└─────────────────┘
vtable:
┌─────────────────┐
│ drop │ → デストラクタ
│ size │ → サイズ
│ align │ → アライメント
│ method1 │ → メソッド1のアドレス
│ method2 │ → メソッド2のアドレス
└─────────────────┘
使い分け
// 静的ディスパッチ(ジェネリクス)
// - コンパイル時に型が決定
// - インライン化可能
// - バイナリサイズ増加
fn process_static<T: Document>(doc: T) {}
// 動的ディスパッチ(トレイトオブジェクト)
// - 実行時に型が決定
// - vtableオーバーヘッド
// - 異なる型を同じコレクションに
fn process_dynamic(doc: &dyn Document) {}
高度なパターン
スーパートレイト
// Debug を実装していないと MyTrait は実装できない
trait MyTrait: Debug {
fn my_method(&self);
}
impl<T: Debug> MyTrait for T {
fn my_method(&self) {
println!("{:?}", self);
}
}
ブランケット実装
// Display を実装している全ての型に ToString を実装
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
マーカートレイト
// 実装なし、型の性質を示す
trait Sendable {}
trait Storable {}
// 複数のマーカーを要求
fn send_and_store<T: Sendable + Storable>(item: T) {}
パフォーマンス考慮事項
インライン化の影響
// ジェネリクス: インライン化される可能性が高い
fn add<T: std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
// トレイトオブジェクト: インライン化されない
fn add_dyn(a: &dyn std::ops::Add<Output = i32>, b: i32) -> i32 {
// 間接呼び出し
}
単相化のコスト
// 型ごとにコード生成
process::<i32>();
process::<i64>();
process::<f32>();
// → 3つの関数が生成される
// バイナリサイズが問題なら dyn を検討
fn process(item: &dyn Display) {
// 1つの関数のみ
}