Are polymorphic variables allowed?
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<dyn 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<dyn 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<dyn 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).
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: &dyn Barks = if 1 == 2 {
dog = Dog;
&dog
} else {
wolf = Wolf;
&wolf
};
animal.bark();
}
It's a bit ugly, but the references accomplish the same indirection as a Box
with a smidge less overhead.
See also:
- How can I conditionally provide a default reference without performing unnecessary computation when it isn't used?
- How do I make format! return a &str from a conditional expression?
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.