I want to address the fundamental misunderstanding here. When a struct holds a borrow, it expects that somewhere else, that value is owned. In languages like C# and Go, that's fine, because the language runtime can keep track of all the references and perform garbage collection. But Rust is not garbage-collected, so you have a few options:
- Option 1: store as a value
This option does not apply to the trait object, but with sized types, this is usually the best approach, since the value is owned by the struct that owns it logically.
- Option 2: store as a borrow
For people used to languages like Go, this is certainly a possibility. Storing a pointer to an interface in Go might seem similar to storing a borrow in Rust, but in reality, by storing a borrow in Rust, that does not make the runtime "keep alive" that borrow. Rust has great ownership semantics via lifetimes so your code will not compile, but it also will not do what you want. We can see an example of this in a language like C++:
// treat this like our "trait"
struct Trait {
virtual void method() = 0;
virtual ~Base() = 0; // best practice
};
// and this is a struct that implements the trait
struct Struct : public Trait {
int some_member;
void method() override { ... }
};
// here we store a reference to something that "implements" the trait
// (since we can't create an instance of Trait due to pure virtuals)
// note the indirection here as well. If we stored a value of Trait,
// we would not be able to store Struct in it, because they are
// different sizes.
struct ReferencesTrait {
Trait& member;
};
// now we can create the ReferencesTrait with a Struct instance
ReferencesTrait create_references_trait() {
Struct on_stack; // this only lives for the duration of the function
// we create a reference to that
// OK as long as we don't use this outside of the function
ReferencesTrait ret{ on_stack };
// oops, dangling reference!
return ret;
}
(Note: Your C++ compiler might give a warning about this, but will not refuse to compile.)
Rust prevents this at compile time. ReferencesTrait
would require a lifetime parameter, and on_stack
would not live long enough to return it from create_references_trait
.
So how can we solve this? We need to avoid storing the struct implementing the trait on the stack. So let's store it on the heap!
- Option 3: store in a
Box<dyn Trait>
This is the most common method for owned trait objects. By heap-allocating the trait object, it is still behind a layer of indirection (Box
stores a pointer), but we are able to own it inside our ReferencesTrait
struct because the pointer itself is sized. However, what if we want to replicate the ability in C# and Go to store the same object in multiple structs?
- Option 4:
Rc
(or Arc
), and maybe some interior mutability
I have tried a lot to share references between multiple structs, but the bottom line is that Rc
or Arc
make it so much easier. No pesky lifetime parameters everywhere! Simply put, Rc
is a form of garbage collection called reference counting, used in languages such as Python. It ensures that, as long as anyone has a reference to the internal value, the value will continue to exist.
Of course, by having multiple references to the same value, it would have to only allow immutable access, which is where interior mutability types like Mutex
, RefCell
, Atomic*
come in. But that's a bit off-topic.
Recap
Basically, when a type is not sized, you have two options: store it as a borrow, or as an owned pointer (Box
, Rc
, Arc
). These simply depend on your use case, but I find owned pointers to be easier to deal with in general.