⚙️

[Rust] トレイトオブジェクト

2023/02/25に公開

はじめに

Rust学習中に気になったので整理します。
https://doc.rust-jp.rs/book-ja/ch17-02-trait-objects.html

トレイトオブジェクトとは

トレイトオブジェクトは、 指定したトレイトを実装するある型のインスタンスを指します。
&参照やBox<T>スマートポインタなどの、 何らかのポインタを指定し、それから関係のあるトレイトを指定することでトレイトオブジェクトを作成します。

trait Article {
    fn show(&self);
}

// Box<dyn Article> がトレイトオブジェクト
struct User {
    articles: Vec<Box<dyn Article>>,
}

トレイトオブジェクトを保持している構造体に対してメソッドを追加。
実際の型はコンパイル時に不明だが、トレイトが実装されていることは保証されているので、メソッドが利用できる。

impl User {
    fn show_all(&self) {
        for article in self.articles.iter() {
            article.show();
        }
    }
}

トレイト境界との違い

似たような実装方法で、ジェネリクスを使ったトレイト境界がある。

struct User<T: Article> {
    articles: Vec<T>,
}

impl<T: Article> User<T> {
    fn show_all(&self) {
        for article in self.articles.iter() {
            article.show();
        }
    }
}

こうすると、全てのコンポーネントの型がButtonだったり、TextFieldだったりするScreenのインスタンスに制限されてしまいます。 絶対に同種のコレクションしか持つ予定がないのなら、ジェネリクスとトレイト境界は、 定義がコンパイル時に具体的な型を使用するように単相化されるので、望ましいです。

→ トレイト境界では型が決まっているので、以下のようになる。

fn main() {
    // Tweetだけ持っているユーザ
    let user: User<Tweet> = User { articles: vec![] };
    // Blogだけ持っているユーザ
    let user: User<Blog> = User { articles: vec![] };
}

TweetとBlog両方持っているユーザを表現できない。

トレイトオブジェクトを使う

ユーザにはTweetとBlog両方を保持させたいので、トレイト境界ではダメだった。
なので、先ほど定義したトレイトオブジェクトのVecにデータを入れていく。

fn main() {
    let user = User {
        articles: vec![
            Box::new(Tweet),
            Box::new(Blog),
            Box::new(Tweet),
            Box::new(Blog),
        ],
    };
    user.show_all();
}

何が違うのか

トレイトオブジェクト

  • 動的ディスパッチ
  • 実行時にポインタ経由で実際の型のメソッドを呼び出す
  • コンパイル時に型が決まっている必要がない
  • 実行時コストがある
  • オブジェクト安全なトレイトのみ
オブジェクト安全?
  • 戻り値の型がSelfでない。
  • ジェネリックな型引数がない。

ざっくり
→トレイトオブジェクトは型が決まってない
→トレイトメソッドでSelfを返す
→Selfが実際何なのか分からない
→NG

詳しくはここ

トレイト境界

  • 静的ディスパッチ
  • コンパイル時に具体的な型に落とし込んだ実装を作ってくれる
    • 実行時コストがない
  • コンパイル時に型が決まっている必要がある

利用例

見かけた利用例を適宜追加していく。

エラー処理
api.rs
use std::error::Error;

pub fn call_api(id: &str) -> Result<(), Box<dyn Error>> {
    let id = id.parse::<u32>()?;
    if !(0..100).contains(&id) {
        Err("Invalid id.")?
    }
    Ok(())
}

パースエラーと業務エラーを返せるように、関数の返り値にトレイトオブジェクトを設定。

main.rs
extern crate sandbox;

fn main() {
    match sandbox::api::call_api("100") {
        Ok(_) => {
            println!("ok");
        }
        Err(e) => {
            println!("API Error! => {:?}", e);
        }
    }
}

Discussion