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