厄介な E0038 エラーRustで動的ディスパッチを使用し、Vec<Box<dyn MyTrait>> に異なる型を格納しようとした際、突然コンパイラに止められることがあります。トレイトが「オブジェクト安全(object safe)」ではないという警告です。ターミナルにはおそらく次のような大量のテキストが表示されます。
error[E0038]: the trait `MyTrait` cannot be made into an object
--> src/main.rs:12:12
|
12 | let obj: Box<dyn MyTrait> = Box::new(MyStruct);
| ^^^^^^^^^^^^^^^^ `MyTrait` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable
原因:オブジェクト安全性を理解するRustは dyn Trait を使用する際、実行時に呼び出す関数を特定するために vtable(仮想関数テーブル)を使用します。vtable は単純な関数ポインタの配列だと考えてください。これが機能するためには、コンパイラがコンパイル時に対象の配列の正確なサイズとレイアウトを把握している必要があります。トレイトが柔軟すぎたり、具体的な型にのみ既知の情報に依存している場合、コンパイラはテーブルを構築できません。この状態になると、そのトレイトはもはや「オブジェクト安全」ではなくなります。
ほとんどのオブジェクト安全性の問題は、以下の4つの具体的な違反に起因します。
- メソッドがジェネリック型パラメータを使用している。- メソッドが
Selfを返している。- メソッドにレシーバ(&selfなど)がない。- トレイトが明示的にSelf: Sizedを要求している。## エラーの解決方法### 1. ジェネリックメソッドの管理Rustは単態化(monomorphization)を通じてジェネリクスを処理します。これは、使用される型ごとに関数のコピーを個別に生成することを意味します。トレイトメソッドがジェネリックである場合、理論上は無限の数の関数を表現することになります。固定サイズの vtable に無限のポインタを収めることはできません。 問題のあるコード:
trait MyTrait {
fn process<T>(&self, data: T); // これによりオブジェクト安全性が失われます
}
解決策: ジェネリックメソッドがトレイトオブジェクトにとって厳密に必要でない場合は、それを隠すことができます。where Self: Sized 境界を追加することで、Rustに「このメソッドは具体的な型にのみ使用させ、vtable には入れない」と伝えることができます。
trait MyTrait {
// このメソッドはトレイトオブジェクトから無視されるようになります
fn process<T>(&self, data: T) where Self: Sized;
// こちらは vtable に残ります
fn execute(&self);
}
2. 返り値としての 'Self' の扱いdyn MyTrait を使用すると、コンパイラは元の型を忘れてしまいます。メソッドが Self を返す場合、コンパイラはどれだけのメモリを割り当てればよいか判断できません。Self が 1バイトの構造体なのか、2キロバイトの配列なのかを知る術がないからです。
問題のあるコード:
trait MyTrait {
fn clone_me(&self) -> Self; // コンパイラは Self のサイズを知ることができません
}
解決策: 生の型を返すのではなく、Box<dyn MyTrait> のようなサイズが既知のポインタを返します。これによりデータはヒープに置かれ、返り値のサイズは一定(64ビットシステムでは通常16バイト)になります。
trait MyTrait {
fn clone_box(&self) -> Box<dyn MyTrait>;
}
3. 'Sized' 要件の削除デフォルトでは、dyn Trait はサイズ不定(?Sized)です。もしトレイトの定義で明示的に Self: Sized を要求している場合、「サイズが既知である必要がある型を求めているが、dyn は実行時までサイズが不明な型に使用される」という矛盾が生じます。
問題のあるコード:
trait MyTrait: Sized {
fn do_work(&self);
}
解決策: 単純にトレイトから Sized 境界を削除してください。特定の1つのメソッドだけがそれを必要とする場合は、トレイト全体ではなく、そのメソッドにのみ境界を適用します。
4. レシーバのないメソッドの修正静的メソッド(new() のようなコンストラクタ)は self パラメータを取りません。インスタンスがないため、参照すべき vtable が存在せず、コンピュータはどの実装を呼び出すべきか判断できません。
trait MyTrait {
fn new() -> Self where Self: Sized; // ここに Sized 境界を追加します
fn run(&self);
}
修正の確認これらの変更を適用したら、具体的な構造体をトレイトオブジェクトに代入してテストしてください。単純な代入が、トレイトがオブジェクト安全になったかどうかを確認する最も早い方法です。
struct Worker;
impl MyTrait for Worker {
fn run(&self) { println!("タスク完了。"); }
fn new() -> Self { Worker }
}
fn main() {
// これがコンパイルできれば成功です!
let my_obj: Box<dyn MyTrait> = Box::new(Worker);
my_obj.run();
}
予防のためのクイックチェックリスト設計に深く入り込む前に、dyn での使用を想定しているすべてのトレイトについて、以下のルールを確認してください。
where Self: Sizedを使用しない限り、メソッドでジェネリックパラメータを使用しない。- 動的ディスパッチを目的とするすべてのメソッドで&self、&mut self、またはBox<Self>を使用していることを確認する。-Selfを直接返さない。- 関連定数を含めない。これらは現在、Rustにおいてオブジェクト安全ではありません。- こまめにcargo checkを実行する。オブジェクト安全性の違反を即座に検出できるため、後で何百行もリファクタリングする手間を省けます。

