Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I do type introspection with trait objects and then downcast it?

Tags:

rust

I have a collection of Trait, a function that iterates over it and does something, and then I would like to check the implementor type and if it is of type Foo then downcast it and call some Foo method.

Basically, something similar to Go's type-switch and interface conversion.

Searching around I found about the Any trait but it can only be implemented on 'static types.

To help demonstrate what I want:

let vec: Vec<Box<Trait>> = //

for e in vec.iter() {
    e.trait_method();

    // if typeof e == Foo {
    // let f = e as Foo;
    // f.foo_method();
    //}
}
like image 526
GGalizzi Avatar asked Jan 11 '15 21:01

GGalizzi


2 Answers

As you have noticed, downcasting only works with Any trait, and yes, it only supports 'static data. You can find a recent discussion on why it is so here. Basically, implementing reflection for references of arbitrary lifetimes is difficult.

It is also impossible (as of now, at least) to combine Any with your custom trait easily. However, a macro library for automatic implementation of Any for your trait has recently been created. You can also find some discussion on it here.

like image 146
Vladimir Matveev Avatar answered Nov 22 '22 06:11

Vladimir Matveev


This isn't a Rust-specific problem, although the vocabulary may be a little different. The ideal way to solve a problem like this, not just with traits in Rust but in any language, is to add the desired behavior (foo_method in your example) to the abstract interface (Trait):

trait Trait {
    fn trait_method(&self);
    fn foo_method(&self) {} // does nothing by default
}

struct Foo;

impl Trait for Foo {
    fn trait_method(&self) {
        println!("In trait_method of Foo");
    }

    fn foo_method(&self) {
        // override default behavior
        println!("In foo_method");
    }
}

struct Bar;

impl Trait for Bar {
    fn trait_method(&self) {
        println!("In trait_method of Bar");
    }
}

fn main() {
    let vec: Vec<Box<dyn Trait>> = vec![Box::new(Foo), Box::new(Bar)];

    for e in &vec {
        e.trait_method();
        e.foo_method();
    }
}

In this example, I have put a default implementation of foo_method in Trait which does nothing, so that you don't have to define it in every impl but only the one(s) where it applies. You should really attempt to make the above work before you resort to downcasting to a concrete type, which has serious drawbacks that all but erase the advantages of having trait objects in the first place.

That said, there are cases where downcasting may be necessary, and Rust does support it -- although the interface is a little clunky. You can downcast &Trait to &Foo by adding an intermediate upcast to &Any:

use std::any::Any;

trait Trait {
    fn as_any(&self) -> &dyn Any;
}

struct Foo;

impl Trait for Foo {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn downcast<T: Trait + 'static>(this: &dyn Trait) -> Option<&T> {
    this.as_any().downcast_ref()
}

as_any has to be a method in Trait because it needs access to the concrete type. Now you can attempt to call Foo methods on a Trait trait object like this (complete playground example):

if let Some(r) = downcast::<Foo>(&**e) {
    r.foo_method();
}

To make this work, you have to specify what type you expect (::<Foo>) and use if let to handle what happens when the referenced object is not an instance of Foo. You can't downcast a trait object to a concrete type unless you know exactly what concrete type it is.

If you ever need to know the concrete type, trait objects are almost useless anyway! You probably should use an enum instead, so that you will get compile-time errors if you omit to handle a variant somewhere. Furthermore, you can't use Any with non-'static structs, so if any Foo might need to contain a reference, this design is a dead end. The best solution, if you can do it, is to add foo_method to the trait itself.

like image 40
trent Avatar answered Nov 22 '22 06:11

trent