How can i do polymorphisme in rust

Hello everyone.. i struggled at understanding traits, especially the part that related to trait object and sized.

assume i have the following case (which does not compile of course) Rust Playground

how can i make it correct?

use std::collections::HashMap;

struct ListOfAnimalsProtected {
    animals: Vec<Animal>
}

impl ListOfAnimalProtected {
    fn push(animal: Animal) {
        self.animals.push(animal)
    }
}

trait Animal {
    fn id() -> usize;
    fn danger_type() -> String;
}

struct Dog;
impl Dog {
    fn id() -> usize { 100 }
    fn danger_type() -> String { "Accident".to_string() }
}

struct Cat;
impl Cat {
    fn id() -> usize { 200 }
    fn danger_type() -> String { "Harasment".to_string() }
}

fn protect(animal: Animal) {
    println!("Protecting {} against {}", animal.id(), animal.danger_type());
}

fn main() {
    let animal: Animal;
    let is_cat = true;
    
    match is_cat {
        true => animal = Cat,
        false => animal = Dog,
    }
    
    protect(animal);
}

In order to be object safe, a trait object cannot contain any generic methods, methods without a self reciever, methods with a by value self reciever, or traits with a Sized bound on the trait.

You can remove these methods specifically for unsized types (like trait objects) by requiring Sized for those methods. Doing this for all of the forbidden methods will make any trait object safe. That said, you won't be able to use those methods via a trait object.

In this case, just add self recievers.

pub trait Animal {
    fn id(&self);

    fn danger_type(&self);
}

If you have a limited set of types that can implement this trait, consider making an enum instead.

enum Animal {
    Dog(Dog),
    Cat(Cat),
}
3 Likes

Maybe the section "Trait objects" from the book (chapter 17.02) can help you to understand your problem and a solution.

1 Like

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 enums 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);
    }
}
7 Likes

Thank you @Yandros , the examples are clear and convenient and it did solve the problem i have with traits :grinning:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.