2

I am new to Rust. My background is Java. I am trying to solve the following problem.

  1. I have a trait Fuel which is implemented for struct Diesel and struct Gas.
  2. I also have a trait Vehicle with Fuel generic type parameter. Vehicle is implemented for struct Car<F: Fuel> and struct Bus<F: Fuel>.

Finally I want to put references to possible heterogeneous trait objects to one Vec like this:

let diesel_car = Car { fuel: Diesel {} };
let gas_car = Car { fuel: Gas {} };
let diesel_bus = Bus { fuel: Diesel {} };
let gas_bus = Bus { fuel: Gas {} };

let garage = vec![
    &diesel_car,
    &gas_car,
    &diesel_bus,
    &gas_bus
];

But I get this compile error:

error[E0308]: mismatched types
  --> src/main.rs:63:9
   |
63 |         &gas_car,
   |         ^^^^^^^^ expected struct `Diesel`, found struct `Gas`
   |
   = note:   expected type `&Car<Diesel>`
           found reference `&Car<Gas>`

What should be the generic type of references in this Vec? It guess it should be something like Vec<&dyn Vehicle>. But this variant doesn't compile either since it wants to know the generic type parameter of Vehicle in advance. In Java I would just write List<Vehicle<?>. Is there anything similar in Rust?

The whole example is available in the playground.

P.S. I can obviously remove a generic parameter from Vehicle and replace it with Box<dyn Fuel>, but I would like to minimize places with dynamic dispatch.

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
Vadim
  • 576
  • 1
  • 4
  • 13

2 Answers2

1

But this variant doesn't compile either since [the compiler] wants to know the generic type parameter of Vehicle in advance.

I think you're confusing static dispatch and dynamic dispatch here. I think you're also confusing what you're asking the compiler to do vs what you expect it to do. It very clearly sounds like you want dynamic dispatch, but you're trying to achieve it using generics, which is not possible because generics are a purely compile-time abstraction and will always produce static dispatch code. "Generics" do not exist at run-time and are not present in the final binary. If you want "run-time generics" that's what trait objects and dynamic dispatch is for.

In Java I would just write List<Vehicle<?>>. Is there anything similar in Rust?

Yes, replace the F: Fuel generic type parameter with a dyn Fuel trait object.

P.S. I can obviously remove a generic parameter from Vehicle and replace it with Box<dyn Fuel>, but I would like to minimize places with dynamic dispatch.

But you just asked how to do the Java equivalent in Rust and that's literally how Java solves this problem: by using dynamic dispatch. You can't have your cake and eat it too. If you want the speed of static dispatch that means also accepting the constraints of static dispatch. If those constraints are too strict for your program then you should use trait objects and dynamic dispatch.

Rust's enums are probably the closest you can get to what you want. They're a pretty good compromise between generics and trait objects, giving you (almost) the speed of the former while affording you the flexibility of the latter. Here's your example refactored to use enums:

enum Fuel {
    Diesel,
    Gas,
}

impl Fuel {
    fn efficiency(&self) -> f64 {
        match self {
            Fuel::Diesel => 0.9,
            Fuel::Gas => 0.8,
        }
    }
}

enum Vehicle {
    Car(Fuel),
    Bus(Fuel)
}

impl Vehicle {
    fn mass(&self) -> f64 {
        match self {
            Vehicle::Car(_) => 1000.0,
            Vehicle::Bus(_) => 5000.0,
        }
    }
}

fn main() {
    let diesel_car = Vehicle::Car(Fuel::Diesel);
    let gas_car = Vehicle::Car(Fuel::Gas);
    let diesel_bus = Vehicle::Bus(Fuel::Diesel);
    let gas_bus = Vehicle::Bus(Fuel::Gas);

    let garage = vec![
        &diesel_car,
        &gas_car,
        &diesel_bus,
        &gas_bus
    ];
}

playground

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
  • Thank you. My example is overly simplified compared to what i want in practice. In practice a want to have a flexibility to add both new fuels and vehicles, so using enums is probably not the nicest way to approach this problem. Moreover, does it have any performance gain compared to using dynamic dispatch? Effectively we just duplicate the logic of dynamic dispatch in a `match` expression, don't we? – Vadim Feb 07 '21 at 13:53
  • 1
    Using `match` saves you from having to dereference a vtable pointer, so I believe it's faster than regular dynamic dispatch. Also, Java uses dynamic dispatch and garbage collection for absolutely everything. So if Java is fast enough for your needs then there is no question that Rust will be fast enough as well, even if you don't use static dispatch. – pretzelhammer Feb 07 '21 at 13:57
0

It is not possible to put different types in the same vector. However, using enums, you can get the functionality you are looking for without needing different types.

Since you have 2 variants of fuel, their enum will look like this:

pub enum Fuel {
    Diesel,
    Gas,
}

You then add a function efficiency to return the efficiency of each fuel variant:

impl Fuel {
    fn efficiency(&self) -> f64 {
        match self {
            Fuel::Diesel => 0.9,
            Fuel::Gas => 0.8,
        }
    }
}

You now have 2 variant of fuel that are of the same type.

After doing the same for vehicles (you have 2 variants of them too), you can add various combinations to the same vector.

Here is a full example.

You can read up on enums here.

Emoun
  • 2,297
  • 1
  • 13
  • 20