2

How can I downcast a trait to a struct like in this C# example?

I have a base trait and several derived structs that must be pushed into a single vector of base traits.

I have to check if the each item of the vector is castable to a specific derived struct and, if yes, use it as a struct of that type.

This is my Rust code, I don't know how to implement the commented part.

trait Base {
    fn base_method(&self);
}

struct Derived1;
impl Derived1 {
    pub fn derived1_method(&self) {
        println!("Derived1");
    }
}
impl Base for Derived1 {
    fn base_method(&self) {
        println!("Base Derived1");
    }
}

struct Derived2;
impl Derived2 {
    pub fn derived2_method(&self) {
        println!("Derived2");
    }
}
impl Base for Derived2 {
    fn base_method(&self) {
        println!("Base Derived2");
    }
}

fn main() {
    let mut bases = Vec::<Box<dyn Base>>::new();
    let der1 = Derived1{};
    let der2 = Derived2{};

    bases.push(Box::new(der1));
    bases.push(Box::new(der2));

    for b in bases {
        b.base_method();
        //if (b is Derived1)
        //  (b as Derived1).derived1_method();
        //else if (b is Derived2)
        //  (b as Derived2).derived2_method();
    }
}

1 Answer 1

6

Technically you can use as_any, as explained in this answer:

How to get a reference to a concrete type from a trait object?

However, type-checking and downcasting when looping over a vector of trait objects is considered a code smell. If you put a bunch of objects into a vector and then loop over that vector, presumably the objects in that vector are supposed to play a similar role.

So then you should refactor your code such that you can call the same method on your object regardless of the underlying concrete type.

From your code, it seems you're purely checking the type (and downcasting) so that you can call the appropriate method. What you really should do, then, is introduce yet another trait that provides a unified interface that you then can call from your loop, so that the loop doesn't need to know the concrete type at all.

EDIT: Allow me to add a concrete example that highlights this, but I'm going to use Python to show this, because in Python it's very easy to do what you are asking to do, so we can then focus on why it's not the best design choice:

class Dog:
  def bark():
    print("Woof woof")

class Cat:
  def meow():
    print("Meow meow")

list_of_animals = [Dog(), Dog(), Cat(), Dog()]

for animal in list_of_animals:
  if isinstance(animal, Dog):
    animal.bark()
  elif isinstance(animal, Cat):
    animal.meow()

Here Python's dynamic typing allows us to just slap all the objects into the same list, iterate over it, and then figure out the type at runtime so we can call the right method.

But really the whole point of well-designed object oriented code is to lift that burden from the caller to the object. This type of design is very inflexible, because as soon as you add another animal, you'll have to add another branch to your if block, and you better do that everywhere you had that branching.

The solution is of course to identify the common role that both bark and meow play, and abstract that behavior into an interface. In Python of course we don't need to formally declare such an interface, so we can just slap another method in:

class Dog:
  ...
  def make_sound():
    self.bark()

class Cat:
  ...
  def make_sound():
    self.meow()

...

for animal in list_of_animals:
  animal.make_sound()

In your Rust code, you actually have two options, and that depends on the rest of your design. Either, as I suggested, adding another trait that expresses the common role that the objects play (why put them into a vector otherwise in the first place?) and implementing that for all your derived structs. Or, alternatively, expressing all the various derived structs as different variants of the same enum, and then add a method to the enum that handles the dispatch. The enum is of course more closed to outside extension than using the trait version. That's why the solution will depend on your needs for that.

2
  • 1
    "The enum is of course more closed to outside extension than using the trait version."—perhaps worth noting the other side of the tradeoff: i.e. enums do not suffer the cost of trait object indirection.
    – eggyal
    Commented Sep 9, 2021 at 9:00
  • Absolutely. Thank you for pointing that out.
    – cadolphs
    Commented Sep 9, 2021 at 15:02

Not the answer you're looking for? Browse other questions tagged or ask your own question.