Using traits inside enums (or how to send different types via mpsc channels)

Hi Folks!

I've run in to a problem with enums and Traits and I have a feeling that I'm missing some significant part of knowledge about enums / traits / types. I'm fairly new to Rust, so it quite possible :slight_smile:

To give you some overview: I'm writing a very simple implementation of actors model. The idea is to have several threads, where different Actors are executed.

Threads are implemented in a way similar to the one described here: https://doc.rust-lang.org/book/ch20-02-multithreaded.html

Every thread has it's own collections of Actors that are executing on it. Actors can receive messages.

Communication between the threads is based on mpsc channels:
let (sender, receiver) = mpsc::channel::<Message>();

Message can be two things:

  • New Actor instance that is "added" to the thread
  • Message to one of the Actors in the thread

I've handled it be making Message an enum

enum Message {
    NewActor(Box<dyn Actor>),
    Task {
        actor_id: ActorId,
        message: String
    }
}

Actor is a simple trait that should be implemented by all Actors
trait Actor {}

And now I have a problem. I constantly run in to different problems trying to define enum with trait as a value of enum variant. Either unknown size (that's why I tried to Box<> it) or, as with the example below (T might not live long enough).

fn add_actor<T: Actor>(&self, actor: T) -> () {
        let message = Message::NewActor(Box::new(actor));
}

I have no idea how to solve it. Should I be using Traits? Should I use dyn Actor, impl? Generics? Am I missing something? Or maybe the whole idea is flawed?

Thank you in advance!

If your challenge is that it might not live long enough, just say that it does:

fn add_actor<T: Actor + 'static>

Hmm, indeed it solves the error. But, does the `static information mean that Actor will live for ever? In reality, the Actor does not have live for the entire life of the application. Actors will be created and destroyed during the application execution.

1 Like

No, it means it can live forever. Whatever holds the value at a given time can choose to drop it or pass it along further.

If it were borrowing something with a shorter lifetime, like some reference &'a X, then it would not be allowed to live any longer than that, making it non-'static.

1 Like

This meaning of static is definitely confusing... I sure was at some point. It more like translates to "if it is or contains a reference, then it must be &'static", however anything that does not contain any references is 'static. It's like a lifetime that groups all references that live for the entire lifetime of the program + anything that has no lifetime (eg. no references).

Another thing you might as well know at once:

Box<dyn SomeTrait>

will default to:

Box<dyn SomeTrait + 'static>

But you can override that one to be another lifetime if you want.

None of it to be confused with the keyword static, which might have been better of being called global or something to avoid the confusion.

In general lifetimes specify an upper bound, but they have no power to enforce the upper bound. So if something is 'static, then it can live forever, but it doesn't have to (as @cuviper said).

Why would T not live long enough? Since it's not a reference; isn't the function holding the value of the parameter and then passing that value to Box::new?

Maybe I am understanding generics in rust wrong, but should the type parameter T not always be an owned value? Seeing that there is no '&' next to T.

The type T could itself be a reference, since &'a u32 is itself a type, and T can be any type implementing the trait. Additionally you can have types such as

struct Data<'a> {
    field: &'a u32
}

which are not references, but nonetheless cannot live forever.

1 Like