In this lesson, you will learn how to combine shared ownership with dynamic dispatch to store heterogeneous types behind a single reference-counted pointer.
The Problem of Heterogeneity
When you use Rc<T>, you are usually restricted to a concrete type T. But what if you have a collection of different objects that all share common behavior? For example, imagine a GUI system where you need to manage multiple Button and Slider components. If you store them in a Vec<Rc<T>>, you are forced to choose one type.
By using Trait Objects (dyn Trait), we can define a shared interface. By wrapping that trait object in Rc, we achieve shared ownership of an abstraction.
Shared Dynamic Dispatch
Instead of storing the concrete type, we store Rc<dyn Widget>. This works because Rc acts as a handle to the heap allocation, and the trait object handles the dynamic dispatch (vtable lookup) required to call methods on the underlying, unknown concrete type.
trait Widget {
fn draw(&self);
}
struct Button;
impl Widget for Button { fn draw(&self) { println!("Drawing button"); } }
fn main() {
// We store different types in the same collection
let components: Vec<Rc<dyn Widget>> = vec![
Rc::new(Button),
// Imagine other types like Slider here...
];
for component in components {
// The Rc provides the pointer, dyn Widget provides the vtable
component.draw();
}
}
Why Compose Them?
The combination of Rc and dyn Trait is powerful for three reasons:
- Polymorphism at Scale: You can pass around a list of objects that share behavior without needing to know their specific sizes or types at compile time.
- Shared Lifecycle: If multiple parts of your application need to "own" a component (e.g., both the layout engine and the event handler need to point to the same
Button),Rcmanages the cleanup automatically. - Decoupling: The code consuming the
Rc<dyn Widget>doesn't need to know how the widget was allocated or what it specifically is, provided it adheres to theWidgettrait.
Key Takeaway: Composing Rc with dyn Trait allows you to create collections of polymorphic objects that share a managed, heap-allocated lifetime.