Are polymorphic variables allowed?

2019-01-04 03:31发布

I have various structs that all implement the same trait. I want to branch on some condition, deciding at runtime which of those structs to instantiate. Then, regardless of which branch I followed, I want to call methods from that trait.

Is this possible in Rust? I'm hoping to achieve something like the following (which does not compile):

trait Barks {
    fn bark(&self);
}

struct Dog;

impl Barks for Dog {
    fn bark(&self) {
        println!("Yip.");
    }
}

struct Wolf;

impl Barks for Wolf {
    fn bark(&self) {
        println!("WOOF!");
    }
}

fn main() {
    let animal: Barks;
    if 1 == 2 {
        animal = Dog;
    } else {
        animal = Wolf;
    }
    animal.bark();
}

标签: rust
3条回答
别忘想泡老子
2楼-- · 2019-01-04 04:10

Yes, but not that easily. What you've written there is that animal should be a variable of type Barks, but Barks is a trait; a description of an interface. Traits don't have a statically-defined size, since a type of any size could come along and impl Barks. The compiler has no idea how big to make animal.

What you need to do is add a layer of indirection. In this case, you can use Box, although you can also use things like Rc or plain references:

fn main() {
    let animal: Box<Barks>;
    if 1 == 2 {
        animal = Box::new(Dog);
    } else {
        animal = Box::new(Wolf);
    }
    animal.bark();
}

Here, I'm allocating the Dog or Wolf on the heap, then casting that up to a Box<Barks>. This is kind of like casting an object to an interface in something like C# or Java, or casting a Dog* to a Barks* in C++.

An entirely different approach you could also use would be enums. You could have enum Animal { Dog, Wolf } then define an impl Animal { fn bark(&self) { ... } }. Depends on whether you need a completely open-ended set of animals and/or multiple traits.

Finally, note that "kind of" above. There are various things that don't work as they would in Java/C#/C++. For example, Rust doesn't have downcasting (you can't go from Box<Barks> back to Box<Dog>, or from one trait to another). Also, this only works if the trait is "object safe" (no generics, no using self or Self by-value).

查看更多
欢心
3楼-- · 2019-01-04 04:25

DK has a good explanation, I'll just chime in with an example where we allocate the Dog or Wolf on the stack, avoiding a heap allocation:

fn main() {
    let dog;
    let wolf;
    let animal: &Barks;
    if 1 == 2 {
        dog = Dog;
        animal = &dog;
    } else {
        wolf = Wolf;
        animal = &wolf;
    }
    animal.bark();
}

It's a bit ugly, but the references accomplish the same indirection as a Box with a smidge less overhead.

查看更多
仙女界的扛把子
4楼-- · 2019-01-04 04:25

Defining a custom enumeration is the most efficient way to do this. This will allow you to allocate on the stack exactly the amount of space you need, i.e. the size of the largest option, plus 1 extra byte to track which option is stored. It also allows direct access without a level of indirection, unlike solutions using a Box or a trait reference.

Unfortunately, it does require more boiler-plate:

enum WolfOrDog {
    IsDog(Dog),
    IsWolf(Wolf)
}
use WolfOrDog::*;

impl Barks for WolfOrDog {
    fn bark(&self) {
        match *self {
            IsDog(ref d) => d.bark(),
            IsWolf(ref w) => w.bark()
        }
    }
}

fn main() {
    let animal: WolfOrDog;
    if 1 == 2 {
        animal = IsDog(Dog);
    } else {
        animal = IsWolf(Wolf);
    }
    animal.bark();
}

In main we use only a single stack allocated variable, holding an instance of our custom enumeration.

查看更多
登录 后发表回答