To expand on @RustyYato's answer, in Rust a trait, i.e., "an interface", is not an actual type.
To simplify the explanation, I will rename your trait Animal
into trait IsAnimal
.
You can express the common type of things that implement the trait/interface but where their original concrete types have been "forgotten" by the compiler with the dyn
keyword: dyn Trait
. So in your case you can unify the type Cat
and the type Dog
within the dyn IsAnimal
type.
But since Rust is very low-level / explicit w.r.t. the memory management and location, it turns out that dyn IsAnimal
cannot be used directly. Indeed, in your example Cat
and Dog
are both zero-sized structs, so they take no space when inlined. But quid of
struct Elephant {
in_this_example_the_elephant_is_big: [u8; 1024],
}
impl IsAnimal for Elephant { ... }
What should be the inline size of the dyn IsAnimal
type? 1024
bytes "just in case"? But then what happens if later on somebody else defines their own IsAnimal
thing, and it is bigger than 1024
bytes? (which is a perfectly legal thing to do with traits). So the answer, is, that, like with the [T]
and str
slice types (note the lack of indirection: I am not talking about &[T]
or &str
!), these types do not have a "fixed size" / a statically known size. And since a compiler would need that knowledge to be able to inline them in the stack, i.e., directly use them, we cannot do that. We say that "the type is not (statically) Sized
" (so it belongs to the category of types that may, or may not, be Sized
, which is named ?Sized
(this is important later with generics, when, in order to include types such as dyn Trait
within a generic <T>
type parameter, the ?Sized
bound needs to be added: <T : ?Sized>
, or impl ... + ?Sized
))
The solution, in this case, is indirection: provided the "object" we are referring to is already allocated somewhere, we can get a (slim) pointer to it:
let cat_ref: &'_ Cat = &Cat;
let cat_mut_ref: &'_ mut Cat = &mut Cat;
let cat_boxed: Box<Cat> = Box::new(Cat); // allocated on the "heap"
// (`Box` could be replaced by an `Rc` or an `Arc`)
And once we have this level of indirection, we can then perform the "unification into dyn Trait
":
let animal_ref: &'_ (dyn IsAnimal) = cat_ref as &'_ (dyn IsAnimal);
let animal_mut_ref: &'_ mut (dyn IsAnimal) = cat_ref as &'_ mut (dyn IsAnimal);
let animal_boxed: Box<dyn IsAnimal> = cat_boxed as Box<dyn IsAnimal>;
-
implementation-wise, this is achieved by fattening the slim pointer with some added metadata (a pointer to a dyn Trait
is not one, but two (slim) pointers wide), which is needed for Rust to be able to use the specific behavior of the original type.
- This is similar to the slim pointer
&[i32; 42]
coercing to the fat pointer &[i32]
by getting an added len: usize = 42
runtime metadata.
In practice, the &'_ (dyn Trait)
and &'_ mut (dyn Trait)
are less commonly used because them being borrowed types hinders their usability (you are only having a borrow over the initial cat, meaning that your "object" is only usable during some limited 'lifetime
, that I have explicitely elided here, by using the anonymous '_
lifetime name).
TL,DR: borrows are complicated and sometimes annoying;
so we stick to Box
for simplicity:
-
let boxed_cat: Box<Cat> = Box::new(Cat);
let boxed_animal: Box<dyn IsAnimal> = boxed_cat as Box<dyn IsAnimal>;
// the previous line is very redundant; so we can reduce it:
let boxed_animal: Box<dyn IsAnimal> = boxed_cat as _; // explicit elision of the coercion
// and once used to this pattern, we can even elide the coercion **implicitly**!
let boxed_animal: Box<dyn IsAnimal> = boxed_cat;
we can even inline everything into:
let boxed_animal: Box<dyn IsAnimal> = Box::new(Cat);
This leads to your Animal
intuition of a type being actually Box<dyn IsAnimal>
:
pub trait IsAnimal {
fn id(&self) -> usize;
fn danger_type(&self) -> String;
}
type Animal = Box<dyn IsAnimal>; // type alias
struct ListOfProtectedAnimals {
animals: Vec<Animal>,
}
impl ListOfProtectedAnimals {
fn push(self: &'_ mut ListOfProtectedAnimals, animal: Animal) {
self.animals.push(animal)
}
}
fn protect (animal: &'_ Animal) // more advanced version: fn protect (animal: &'_ (impl IsAnimal + ?Sized))
{
println!("Protecting {} against {}", animal.id(), animal.danger_type());
}
struct Cat;
impl IsAnimal for Cat { ... }
struct Dog;
impl IsAnimal for Dog { ... }
fn main ()
{
let animal: Animal;
let is_cat = true;
match is_cat {
true => animal = Box::new(Cat), // indirection required
false => animal = Box::new(Dog), // ditto
}
let mut list_of_protected_animals = ListOfProtectedAnimal { animals: vec![] };
list_of_protected_animals.push(animal);
for animal in &list_of_protected_animals.animals {
protect(&animal);
}
}
All that having been said, you may find working with enum
s more convenient (you don't even need traits in that case
enum Animal { // an `enum` is a conjunction, _i.e._, an Animal is either...
// ... a Dog
Dog(Dog), // The first `Dog` refers to the name of the case / tag / kind / variant, then the second `Dog` refers to the actual type of the value contained in this case
// ... or a Cat:
// tag(value_type)
// vvv vvv
Cat(Cat),
// etc. (we could later support more animals by adding them here)
}
struct ListOfProtectedAnimals {
animals: Vec<Animal>,
}
impl ListOfProtectedAnimals {
fn push(self: &'_ mut ListOfProtectedAnimals, animal: Animal) {
self.animals.push(animal)
}
}
fn protect (animal: &'_ Animal)
{
match animal {
// case value-associated-with-that-case
// vvv vvv
| Animal::Dog(dog) => {
println!("Protecting a 🐕 against {}", dog.danger_type());
},
// case value-associated-with-that-case
// vvv vvv
| Animal::Cat(cat) => {
println!("Protecting a 🐈 against {}", cat.danger_type());
},
}
}
struct Cat;
impl /* IsAnimal for */ Cat {
// no need for a trait!
// Cat-specific behavior
}
struct Dog;
impl /* IsAnimal for */ Dog { /* ditto for Dog */ }
fn main ()
{
let animal: Animal;
let is_cat = true;
match is_cat {
// tag value
// vvv vvv
true => animal = Animal::Cat(Cat),
false => animal = Animal::Dog(Dog),
}
let mut list_of_protected_animals = ListOfProtectedAnimal { animals: vec![] };
list_of_protected_animals.push(animal);
for animal in &list_of_protected_animals.animals {
protect(&animal);
}
}