🚧

【Rust】抽象化のテクニック

に公開

はじめに

  • cargo 1.87.0 (99624be96 2025-05-06)
  • edition = "2024"

トレイト

Rustのトレイトは他言語のインタフェースと似ており、構造体に実装することで異なる型の振る舞いを共通化することができます。

trait Animal {
    fn make_sound(&self) -> String;
}
トレイトの実装
struct Dog;

impl Animal for Dog {
    fn make_sound(&self) -> String {
        "Woof".into()
    }
}

fn main() {
    print_sound(Dog);
}

静的ディスパッチ

fn print_sound<T: Animal>(animal: T) {
    println!("{}", animal.make_sound());
}

このコードは静的ディスパッチであり、コンパイル時に呼び出されるメソッドが決定されるため、効率的に実行することができます。

動的ディスパッチ

一方で、追加の実行コストを払う代わりに動的ディスパッチを利用することで、より柔軟にコードを組み立てることもできます。

struct Cat;

impl Animal for Cat {
    fn make_sound(&self) -> String {
        "Meow".into()
    }
}

fn print_sounds(animals: Vec<&dyn Animal>) {
    for animal in animals {
        println!("{}", animal.make_sound());
    }
}

fn main() {
    print_sound(Dog);
    print_sounds(vec![&Cat, &Dog])
}

Dog構造体に加えてCat構造体を追加し、Animalトレイトを実装した型の配列を受け取るコードです。これは、動的ディスパッチされたトレイトがメソッドの呼び出し名と関数ポインタのマップ(vtable)を保持しているため実現できます。

コンパイルエラー
-fn print_sounds(animals: Vec<&dyn Animal>) {
+fn print_sounds(animals: Vec<&impl Animal>) {
     for animal in animals {
         println!("{}", animal.make_sound());
     }
 }

 fn main() {
     print_sound(Dog);
     print_sounds(vec![&Cat, &Dog])
 }
error[E0308]: mismatched types
  --> src/main.rs:33:29
   |
33 |     print_sounds(vec![&Cat, &Dog])
   |                             ^^^^ expected `&Cat`, found `&Dog`
   |
   = note: expected reference `&Cat`
              found reference `&Dog`

ブランケット実装

ブランケット実装はトレイトを自動実装させる構文です。

Animalトレイトに続いてCarnivore[1]トレイトを定義し、Catへ実装します。

trait Carnivore {
    fn eat<T: Animal>(&self, animal: T);
}

impl Carnivore for Cat {
    fn eat<T: Animal>(&self, animal: T) {
        drop(animal);  // 所有権システムによりこのコードに意味はありません。
    }
}

さらに、肉食動物を表すトレイトも定義していきます。

trait CarnivoreAnimal: Animal + Carnivore {}

このときに、ブランケット実装を定義することで、AnimalCarnivoreを実装している型に対して、自動的にCarnivoreAnimalも実装させることができます。

impl<T: Carnivore + Animal> CarnivoreAnimal for T {}
食物連鎖を関数で表現する例
fn food_chain<T, U>(predator: T, prey: U)
where
    T: CarnivoreAnimal,
    U: Animal,
{
    predator.eat(prey);
    predator.make_sound();
}

非同期でトレイト

Carnivoreトレイトを非同期で実行するように変更します[2]

trait Carnivore {
    fn eat<T>(&self, animal: T) -> Pin<Box<dyn Future<Output = ()>>>
    where
        T: Animal + 'static;
}

'staticライフタイムが要求されていますが、ここで重要なのは参照が有効であることではなく、参照を含んでいないことです。

実装は次のようになります。

impl Carnivore for Cat {
    fn eat<T>(&self, animal: T) -> Pin<Box<dyn Future<Output = ()>>>
    where
        T: Animal + 'static,
    {
        Box::pin(async move {
            drop(animal);
        })
    }
}

#[tokio::main]
async fn main() {
    Cat.eat(Mouse).await;
}

参考

脚注
  1. 肉食の意 ↩︎

  2. これを定義するのは冗長なのでfuturesクレートなどを利用してください ↩︎

Discussion