Re-exposing Traits
Sometimes we use an enum to abstract over multiple types that implement the same trait, and want the enum to also re-expose the trait’s interface.
trait Pet {
fn talk(&self);
}
struct Dog {
name: String,
}
struct Cat {
age: u8,
}
impl Pet for Dog {
fn talk(&self) {
println!("Woof! My name is {}~!", self.name);
}
}
impl Pet for Cat {
fn talk(&self) {
println!("Meow! I am {} years old", self.age);
}
}
enum AnyPet {
Dog(Dog),
Cat(Cat),
}
impl Pet for AnyPet {
fn talk(&self) {
match self {
Self::Dog(dog) => dog.talk(),
Self::Cat(cat) => cat.talk(),
}
}
}
fn do_pet_stuff(pet: &impl Pet) {
pet.talk();
}
fn main() {
let cat = Cat { age: 19 };
let dog = Dog { name: "Fido".into() };
do_pet_stuff(&cat);
do_pet_stuff(&dog);
do_pet_stuff(&AnyPet::Dog(dog));
do_pet_stuff(&AnyPet::Cat(cat));
}
-
One drawback of using an enum for dynamic polymorphism is that if our underlying types (
CatandDogin this case) implement a trait (Pet), our wrapper enum doesn’t automatically expose that same trait interface. -
We can generally implement the trait for the wrapper enum by matching on the enum and dispatching to the corresponding trait method on the underlying types.
-
This is dynamic dispatch, but using the enum’s discriminant instead of a vtable in order to lookup the correct function to call.
-
This is an ergonomic drawback of an enum vs
dyn: The implementation ofPetforAnyPetis pure boilerplate that we need to repeat each time we have a situation like this, whereasdyngives us this behavior purely from thePetimpls onCatandDog.- To avoid writing this boilerplate ourselves, we can use a crate like enum_dispatch, which handles the work of generating the wrapper enum and implementing the trait.
-
The advantage of this approach is that we retain the useful properties of an enum (e.g. the ability to pattern match on it) while also exposing a way to do dynamic dispatch through the trait’s interface.
-
This also enables us to use
AnyPetwith generic functions likedo_pet_stuff, which we can also do withdyn.