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 Box
es 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
Bird
s. Otherwiseenum
fits much better. - However, all
Bird
s in this hierarchy have exactly the same set of fields andtrait
s supported. Otherwise, you have to usetrait
s and standard dynamic dispatch viaBox<dyn Bird>
. - You don't want heap allocation for individual
Bird
s. Otherwise, you may usetrait
s and standard dynamic dispatch.