👊
Rust で Dispatch をする 3 つの方法
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>
のT
はMemStorage
かFileStorage
のどちらであるかがコンパイル時に決まっており、T
にMemStorage
かFileStorage
か決まってないオブジェクトを入れることはできない。
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