👊

Rust で Dispatch をする 3 つの方法

2024/01/27に公開

Rust には大きく分けて Static Dispatch と Dynamic Dispatch の二つの Dispatch 方法があります。本記事では、Rust で Dispatch を実装する 3 つの方法を示します。3 つの手法のうち 1 つが Static Dispatch で 2 つが Dynamic Dispatch です。

Static Dispatch

use std::collections::HashMap;

pub trait Storage {
    fn new(storage_name: &str) -> Self;
    fn put(&mut self, key: &str, value: &str);
    fn get(&self, key: &str) -> Option<&str>;
}

pub struct MemStorage {
    data: HashMap<String, String>,
}

impl Storage for MemStorage {
    fn new(_storage_name: &str) -> Self {
        MemStorage {
            data: HashMap::new(),
        }
    }

    fn put(&mut self, key: &str, value: &str) {
        self.data.insert(key.to_string(), value.to_string());
    }

    fn get(&self, key: &str) -> Option<&str> {
        self.data.get(key).map(|s| s.as_str())
    }
}

pub struct FileStorage {
    storage_name: String,
}

impl Storage for FileStorage {
    fn new(storage_name: &str) -> Self {
        FileStorage {
            storage_name: storage_name.to_string(),
        }
    }

    fn put(&mut self, key: &str, value: &str) {
        unimplemented!()
    }

    fn get(&self, key: &str) -> Option<&str> {
        unimplemented!()
    }
}

pub struct StorageManager<T: Storage> {
    storages: HashMap<String, T>,
}

impl<T: Storage> StorageManager<T> {
    pub fn new() -> Self {
        StorageManager {
            storages: HashMap::new(),
        }
    }

    pub fn put(&mut self, storage_key: &str, key: &str, value: &str) {
        let storage = self
            .storages
            .entry(storage_key.to_string())
            .or_insert_with(|| T::new(storage_key));
        storage.put(key, value);
    }

    pub fn get(&self, storage_key: &str, key: &str) -> Option<&str> {
        let storage = self.storages.get(storage_key).unwrap();
        storage.get(key)
    }
}

fn main() {
    let mut sm = StorageManager::<MemStorage>::new();
    // let mut sm = StorageManager::<FileStorage>::new();
    sm.put("table", "key", "value");
    sm.get("table", "key");
}

特徴

  • Static に Dispatch をするので、コンパイル時にどの Storage を使うか決めなければいけない。
  • 複数種類の Storage (この場合 MemStorage, FileStorage) を Dynamic なデータ構造 (Vec, HashMap, BTree, など) に入れることはできない。上の例で言うと HashMap<String, T>TMemStorageFileStorage のどちらであるかがコンパイル時に決まっており、TMemStorageFileStorage か決まってないオブジェクトを入れることはできない。

Box<dyn Trait> を使った Dynamic Dispatch

// example of dynamic dispatch
pub trait Animal {
    fn cry(&self);
}

pub struct Dog;

impl Animal for Dog {
    fn cry(&self) {
        println!("Bark");
    }
}

pub struct Cat;

impl Animal for Cat {
    fn cry(&self) {
        println!("Meow");
    }
}

pub struct AnimalHouse {
    animals: Vec<Box<dyn Animal>>,
}

pub fn main() {
    let mut animal_house = AnimalHouse { animals: vec![] };
    animal_house.animals.push(Box::new(Dog));
    animal_house.animals.push(Box::new(Cat));

    for animal in animal_house.animals.iter() {
        animal.cry();
    }
}

Out:

Bark
Meow

特徴

  • Dynamic に Dispatch をするので、Box<dyn Animal>Box<dyn Dog>Box<dyn Cat> かは実行時に判断される。いわゆる vtable lookup をするので、実行時にオーバーヘッドが乗る。
  • Static Dispatch と違い、データ構造に Animal 型 (Box<dyn Animal>) を入れることができる。この Box<dyn Animal> 型が実際に Box<dyn Dog> なのか Box<dyn Cat> なのかはコンパイル時に判明していなくて良い。

Enum を使った Dynamic Dispatch

pub enum Animal {
    Dog,
    Cat,
}

impl Animal {
    pub fn cry(&self) {
        match self {
            Animal::Dog => println!("Bark"),
            Animal::Cat => println!("Meow"),
        }
    }
}

pub struct AnimalHouse {
    animals: Vec<Animal>,
}

pub fn main() {
    let mut animal_house = AnimalHouse { animals: vec![] };
    animal_house.animals.push(Animal::Dog);
    animal_house.animals.push(Animal::Cat);

    for animal in animal_house.animals.iter() {
        animal.cry();
    }
}

Out:

Bark
Meow

特徴

  • Enum だと他と比べて少ないコード量で Dispatch を記述できるが、 Animal のメソッドが増えれば増えるほど、メソッドごとに match 文を書かないといけないので、Animal Enum のコードが煩雑になる。
  • Enum の match 文が Box<dyn Trait> の vtable lookup に対応していると言える。
  • Enum は Box<dyn Trait> と異なり、Heap ではなく Stack にオブジェクトが展開されるので、Box<dyn Trait> よりオーバーヘッドは小さいと考えられる。
  • Static Dispatch と違い、データ構造に Animal 型 (Animal) を入れることができる。この Animal 型が実際に Animal::Dog なのか Animal::Cat なのかはコンパイル時に判明していなくて良い。

Discussion