トレイトオブジェクトについて理解する

rust

トレイトオブジェクトをひとことで

あるトレイトを実装した型のインスタンスを指すもの

トレイトオブジェクトを図示してみる

例えば、「Drinkable(飲める)」という特性を持った(トレイトを実装した)インスタンスを考えてみます。これを図示してみると以下のようになると思います。

トレイトオブジェクトを表す記号として、Box<dyn Drinkable>と書きます。これにはDrinkableを実装したインスタンスを指すアドレスが入っています。dyn Drinkableは「Drinkableを実装した型」を表します。今回は適当に0x12345と書きました。

トレイトオブジェクトを使うことの嬉しさ

トレイトオブジェクトを使うとどんな利点があるの?と思うかもしれません。トレイトオブジェクトを使うことで、異なる型を「同じメソッドを持つもの」として扱うことができます。これによりトレイトが持つ共通のメソッド(振る舞い)に対して処理を書くことができます。例えばDrinkableトレイトがdrinkメソッドを持つとき、drinkは存在することを前提として処理を書くことができます。

trait Drinkable {
    fn drink(&self) {}
}

struct Coke();

impl Drinkable for Coke {
    fn drink(&self) {
        println!("drink Coke!");
    }
}

struct Tea();

impl Drinkable for Tea {
    fn drink(&self) {
        println!("drink Tea.");
    }
}

fn main() {
    let drinks: Vec<Box<dyn Drinkable>> = vec![Box::new(Coke()), Box::new(Tea())];
    for some_drink in drinks {
        some_drink.drink();
    }
}

実行結果

drink Coke!
drink Tea.

こちらの例では、Drinkableを実装する構造体として、CokeとTeaを定義しました。main関数でこれらのインスタンスを持つVecを定義しています。これらは異なる型なので通常は同じVecの要素にはなりえませんが、Box<dyn Drinkable>を構造体要素とすることで、「Drinkableを実装した型のインスタンスへのアドレス」のVecとして同じVecの要素として扱うことができます。

また、main関数内のfor文でそれぞれの要素に対して、drinkメソッドを呼び出しています。Vec内の要素はDrinkableが実装されているので、drinkメソッドも呼び出せるというわけです。

トレイトオブジェクトを使うと実行速度が遅くなる

実行速度が遅くなるらしいです。

トレイトオブジェクトで異なる型の値を許容する - The Rust Programming Language 日本語版

具体的な理由はわかりませんが、これは「rustが実行時に型を決めることによって実行速度を速くしている」ということだと思います。例えばジェネリクスを使う際、rustはコンパイル時にどの型でジェネリクスが使用されているか特定し、その型に対するコードを生成します。

トレイトオブジェクトを使うと、実行するまでどの型を参照するか特定できません。すると、実行時に型を特定し、その型がどこにあるのかを探すので実行速度が遅くなるのかなと思います。

以上、推測でした。

上記で出てきたジェネリクスについては、別に記事を書いています。

最後に

トレイトオブジェクトはコードに柔軟性をもたらすことができる反面、実行速度が遅くなるという特徴を持っているということがわかりました。といっても、rustは他言語と比較しても実行速度が速いので、多少の実行速度の低下はそれほど問題にならないと思っています。使えるタイミングがあれば使っていきたいです。

ちなみにトレイトオブジェクトは、特にエラーハンドリングでよく使われる印象です。エラーハンドリングについては別に記事を書こうと思います。

コメント

タイトルとURLをコピーしました