🚧
【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 {}
このときに、ブランケット実装を定義することで、Animal
とCarnivore
を実装している型に対して、自動的に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;
}
参考
- The Rust Programming Language 日本語版. "トレイト: 共通の振る舞いを定義する"
- Rust Documentation. "keyword dyn"
- エディションガイド. "新しいキーワード"
Discussion