Home > front end >  Rust - Same Type different behaviour
Rust - Same Type different behaviour

Time:11-05

As far as my understanding goes, generics allow the same behavior to be shared among different types. For instances,

trait Bird {}

struct BirdFly {}

impl Bird for BirdFly  {
   pub fn fly() -> can fly
}

struct BirdCanntFly {}

impl Bird for BirdCanntFly{
   pub fn fly() -> cannt fly
}

let birds = vec![
    Box::new(BirdFly{}),        // allow this bird to fly, for instance
    Box::new(BirdCanntFly{}),   // dont't allow this bird to fly
];

My question is about the other way around, i.e., is it possible to make the same type take different behaviors (without ifs, enums, nor Box).In this example, it seems a waste to have a Box, BirdFly and BirdCanntFly when in both types (BirdFly and BirdCanntFly) the dimension is the same, only the behavior is different. Something like:

struct Bird {
   fly: // associate different behavior
}

let birds = vec![
    Bird{ fly: some_fly_behavior },      // allow this bird to fly, for instance
    Bird{ fly: another_fly_behavior },   // dont't allow this bird to fly
];

birds[0].fly();
birds[1].fly();

if fly receives the same arguments and returns the same type I can't see a reason for the issue. Furthermore, this way I could get rid of the Box inside the vector. Especially because I may have millions of elements inside the vector and are accessed multiple times iteratively and this way I would avoid the overhead. Thanks for the help!

CodePudding user response:

You can store function pointer inside the struct Bird and add a helper method to get the syntax you want.

struct Bird {
    name: String,
    fly_impl: fn(&Bird) -> (),
}

impl Bird {
    fn new(name: String, fly_impl: fn(&Bird) -> ()) -> Bird {
        Bird { name, fly_impl }
    }

    fn fly(self: &Bird) {
        (self.fly_impl)(self)
    }
}

fn behavior1(b: &Bird) {
    println!("{} behavior1", b.name);
}

fn behavior2(b: &Bird) {
    println!("{} behavior2", b.name);
}

fn main() {
    let captured_variable = 10;
    let birds = vec![
        Bird::new("Bird1".into(), behavior1),
        Bird::new("Bird2".into(), behavior2),
        Bird::new("Bird3".into(), |b| println!("{} lambda", b.name)),
        /*Bird::new("Bird4".into(), |b| {
            println!("{} lambda with {}", b.name, captured_variable)
        }),*/
    ];
    (birds[0].fly_impl)(&birds[0]);
    for bird in birds {
        bird.fly();
    }
}

fn is not to be confused with trait Fn. The former allows for functions and non-capturing lambdas only and has fixed size. The latter allows for arbitrary captures of arbitrary size, hence requiring some indirection like Box. Both do dynamic dispatch when called.

Note how Bird3's behavior is specified by a non-capturing lambda, this is allowed. However, if you try to uncomment Bird4, the example won't compile as its desired behavior is a capturing lambda, which can grow arbitrary big in general and we've banned Boxes together with other indirection.

I don't know enough Rust to tell you whether there is a more Rust-like solution. However, your situation is quite specific:

  • You have an arbitrary extendable hierarchy of Birds. Otherwise enum fits much better.
  • However, all Birds in this hierarchy have exactly the same set of fields and traits supported. Otherwise, you have to use traits and standard dynamic dispatch via Box<dyn Bird>.
  • You don't want heap allocation for individual Birds. Otherwise, you may use traits and standard dynamic dispatch.
  • Related