It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

トレイトオブジェクトとトレイト境界の入門

Rustのトレイトオブジェクトとトレイト境界について整理。

トレイト(trait)

doc.rust-jp.rs

  • 任意の型となりうるSelfに対して定義されたメソッドの集合
  • 多言語、例えばJavaで言えばInterfaceのようなもの
trait Speak {
  fn speak(&self);
}

トレイトオブジェクト

doc.rust-jp.rs doc.rust-lang.org

  • あるトレイトを実装した構造体(インスタンス)とtraitメソッドを検索するためのテーブルを指すとある。
impl Speak for Dog {
  fn speak(&self) {
    println!("Woof!");
  }
}

fn main() {
  // このanimalがトレイトオブジェクト
  let animal: Box<dyn Speak> = Box::new(Dog);
  animal.speak();
}
  • 上記例でいうと、Box型のanimalがトレイトオブジェクトになる。
  • トレイトオブジェクトは、&またはBoxのようなスマートポインタを使って dyn キーワードでトレイトの型を指定する必要がある。

トレイトオブジェクトを使うとポリモーフィズムが実現できる

trait Speak {
  fn speak(&self);
}

struct Dog;
struct Cat;

impl Speak for Dog {
  fn speak(&self) {
    println!("Woof!");
  }
}

impl Speak for Cat {
  fn speak(&self) {
    println!("Meow!");
  }
}

fn main() {
  let animals: Vec<Box<dyn Speak>> = vec![Box::new(Dog), Box::new(Cat)];
  // 実際に中に入っている型は異なるが、同じSpeakトレイトを実装しているため、speakメソッドを呼べる
  for animal in animals {
    animal.speak();
  }
}

トレイト境界

  • ジェネリクス型のパラメタを使い特定のトレイトが実装されているものだけに制限(制約)を掛ける
trait Speak {
  fn speak(&self);
}

struct Dog;

impl Speak for Dog {
  fn speak(&self) {
    println!("Woof!");
  }
}

// Speakトレイトを実装している構造体のみこの関数にわたすことができる
fn speak_animal<T: Speak>(animal: T) {
  animal.speak();
}

fn main() {
    speak_animal(Dog);
}

github.com

  • 境界という言葉に馴染みが持てなかったが、シンプルにジェネリクスによる制約をつけるという理解で良さそう?

トレイトオブジェクトとトレイト境界の違い

doc.rust-jp.rs

トレイトオブジェクトもトレイト境界もトレイト実装の構造体を絞り込めるが、実行時の挙動差異がある。

  • ジェネリクス型を使ったトレイト境界は、コンパイル時に具体的な型(構造体)に対してジェネリクス定義ではない固有の実装としてコード生成される(スタティックディスパッチ)
  • トレイトオブジェクトを使った場合、コンパイラは実行時にどのメソッドを呼び出すか決め、コードを生成する。(ダイナミックディスパッチ)
    • 実行時コストがかかる

トレイトオブジェクトの制限

  • トレイト実装の戻り値の型がSelfではいけない

トレイトオブジェクトをダウンキャストする

Javaなどと同じ感覚でInterfaceとして、トレイトを扱おうとするとダウンキャストしたくなるが、トレイトオブジェクトをダウンキャストするのは、そこまで簡単ではない。

ダウンキャストするためには、std::any::Any トレイトの実装とdowncast_refを使う。

dyn <Trait> 型のトレイトオブジェクトになった時点で型情報が失われているため、これを一旦Any型にし、downcast_refで指定した型情報の参照に動的に変換する必要がある。

(downcast_refは、Option<&T>型を返す)

use std::any::Any;

// Anyトレイトを実装する必要がある
trait Speak: Any {
  fn speak(&self);
  fn as_any(&self) -> &dyn Any;
}

struct Dog {
  name: String,
}

struct Cat {
  name: String,
}

impl Speak for Dog {
  fn speak(&self) {
    println!("Woof!");
  }
  // an_anyで&dyn Anyで自身を返す
  fn as_any(&self) -> &dyn Any {
    self
  }
}

impl Speak for Cat {
  fn speak(&self) {
    println!("Meow!");
  }

  fn as_any(&self) -> &dyn Any {
    self
  }
}

fn main() {
  let animals: Vec<Box<dyn Speak>> = vec![
    Box::new(Dog {
      name: "dog".to_string(),
    }),
    Box::new(Cat {
      name: "cat".to_string(),
    }),
  ];
  for animal in animals {
    // Some<&Dog>が返るのでアンラップできればダウンキャストで型付できている
    if let Some(dog) = animal.as_any().downcast_ref::<Dog>() {
      println!("{}", dog.name);
    } else if let Some(cat) = animal.as_any().downcast_ref::<Cat>() {
      println!("{}", cat.name);
    }
  }
}
  • &dyn Anyに変換するということは、Any型のトレイトオブジェクトにしてからダウンキャストするという理解。

感想

  • ダウンキャストは、クラス-インスタンスの多言語だと比較的カジュアルにやるが、Rustの言語モデルでいうと合っていなそうなのでなるべく別の選択肢をとる方が良さそう。
  • トレイトオブジェクト自体も動的なコストが発生するため、なるべく選択しないほうがよいのかもしれない。