Trait Object or Enum, how to choice

I want to implement an application that has different backends for querying data from various sources. All these backends are implemented BackendTrait trait. These backends are created during the initialising time, and I want to store them in a vector.

Two options are available: using trait object or enum.

I always find trait objects to be heavy and not entirely aligned with Rust's philosophy. However, by using enum, each time I want to invoke trait method of a type, I need to match it first, then I can call the trait function.

For example, I have struct Rsync and struct Ftp backends, and all of them are implemented BackendTrait:

trait BackendTrait {
    fn upload();
}

Then I have this enum:

enum Backend {
    FTP(Ftp),
    RSYNC(Rsync),
}

These two backends are stored in a vector like this:

let ftp = Ftp{};
let rsync = Rsync{};
let backends = vec![Backend(ftp), Backend(rsync)];

Then, call upload() function for each of these backends:

for b in backends.iter() {
    match b {
        FTP(b) => b.upload(),
        RSYNC(b) => b.upload(),
    }
}

The mach{} here looks verbose here.

It is good to use trait object replace enum here?
Or, there are some magic ways to remove the match{} clause,

If you want to effectively perform dynamic dispatch on unrelated types, then do use a trait object. A "backend" is typically something that should be open to extension, so it's much better represented as a trait than with an enum.

3 Likes

The repeated matches can be a little irritating, but it's not that bad.

In regards to which to pick in general: if you only intend to support a limited set of backends, use an enum. If you want it to be open to extension, use a trait.

Or... why not both?

enum Backend {
    Ftp(Ftp),
    Rsync(Rsync),
    Other(Box<dyn DynBackend>),
}
1 Like

I'm a fan of the enum approach. You should implement BackendTrait for the enum to make it easier to work with.

impl BackendTrait for Backend {
    fn upload(&self) {
        match self {
            Ftp(ftp) => ftp.upload(),
            Rsync(rsync) => rsync.upload(),
        }
    }
}

Making a variant with a trait object isn't great because you get the downsides of both trait objects and enums unless you're using it as a rare branch. Just make your functions generic so that you can use Vec<Ftp>, Vec<Rsync>, Vec<Box<dyn BackendTrait>>, or Vec<Backend>.

3 Likes

Note that you can (usually) also implement your trait on the enum itself - and avoid the verbosity of having to use match everywhere.

2 Likes

It seems to be a good solution. Thank you.

Because if you want to allow extension by adding a trait object variant, then you could have just used the trait object in the first place for the other cases as well, and the enum only adds unnecessary complexity and indirection.

1 Like

Trait objects are a part of Rust so how could they not be "aligned with [its] philosophy"? With the enum you're basically creating the same thing: instead of an extra word for the vtable you need an extra word to tag the type.

Because it is actually a 'neither'.

If you use enum, it is easy to add extra functionality (set of variants is fixed, just implement it for all of them), but adding extra variant would break user's code (set of functions matching on the enum is open and out of anyone's control, they would not get automatically implemented for the new variant).

If you use trait objects, adding extra 'variant' is easy (set of methods is fixed, just implement all of them) while adding extra functionality would break user's code (set of implementors of the trait is open and out of anyone's control, the new method would not get automatically implemented).

With your suggestion you get disadvantages of both approaches (both adding a variant and adding a method would break user's code).

3 Likes

It's just like how raw pointers are usually not idiomatic. Trait objects and pointers are both for specific situations but can technically work for more than that. Trait objects enable runtime trait polymorphism, but they only exist because there are some downsides to compile-time polymorphism.

In this case, the only downside of the enum is having to write a match for every trait method. This is a pretty small downside, especially with macros available.

There's no apparent issue with code size, and since it's an application, there's no need for ergonomics for users of a library; any extension can be made on the enum itself.

No, there are situations where compile-time polymorphism is impossible. Trait objects are necesary, and it's perfectly fine to use them. They are nothing like raw pointers or unsafe code.

1 Like

That is what I meant by downsides. This is not one of those situations.

That's also true of raw pointers and unsafe code.

Which one? I don't get what you are referring to here. Trait objects would be a perfectly fine solution to OPs problem, as they want dynamic dispatch.

The original post at the top. It is not impossible with enums.

This sounds like reinventing the wheel. There are other downsides of an enum approach besides all the typing (which is a significant downside because you and your colleagues are going to have to read all that boilerplate): for instance, it wastes memory, because in this case every value is the size of the larger variant.

dyn Trait has a really undeserved bad reputation. I think this stems from a C++ guideline that says you should avoid virtual.

1 Like

It's not impossible with enums, but impossible with compile-time polymorphism. Also, a trait object is likely a better solution in this case than an enum would be.

There was some conversation about this in another thread recently that might be of interest to you:

2 Likes

It will waste memory if the largest type is larger than a pointer (with at least the same alignment). Pointers aren't huge, and memory isn't always an issue, but it's something to consider. You can always make the memory usage the same by boxing the large variants. And they're not even the same memory: the enum has the data inline. You can't do that with trait objects.

If I'm boxing the enum individually, I'd definitely try changing it to a trait object.

But really, I would recommend the enum over a trait object even if they compiled to the same thing. Half the questions on here are quirks about trait objects that enums don't have to worry about, like the hidden lifetime parameter (any lifetimes on the enum will be obvious), DST things (enums are statically sized), and conversion (enums are built like any other value). My only controversial take is that enums being easier to understand outweighs the advantages of trait objects in most situations.

The important part is that well-written code can take either. Then you can decide if the performance demands enums, or if trait objects will provide valuable ergonomics.

P.S. I never learned much C++ so that's not it, at least for me.