Understanding Rust's Trait Objects: The Tale of Two References
When working with Rust's trait objects, you might encounter an interesting difference in behavior between &dyn Trait
and Box<dyn Trait>
. Let's explore this subtle but important distinction that reveals much about Rust's type system and safety principles.
A Tale of Two References
Let's start with a simple example that demonstrates this difference:
trait A {
fn do_something(&self);
}
trait B {
fn do_something_else(&self);
}
// Implementing B for both reference types
impl B for &dyn A {
fn do_something_else(&self) {
self.do_something();
println!("Doing something else!");
}
}
impl B for Box<dyn A> {
fn do_something_else(&self) {
self.do_something();
println!("Doing something else!");
}
}
struct Concrete;
impl A for Concrete {
fn do_something(&self) {
println!("Concrete does something!");
}
}
fn main() {
let concrete = Concrete;
// Case 1: &dyn A
let a_ref: &dyn A = &concrete;
a_ref.do_something_else(); // This works
// let b_ref: &dyn B = a_ref; // This fails!
// Case 2: Box<dyn A>
let a_box: Box<dyn A> = Box::new(concrete);
a_box.do_something_else(); // This works
let b_ref: &dyn B = &a_box; // This also works!
}
The Key Difference
The fascinating part is that while both types can call methods directly, they behave differently when it comes to creating new trait object references:
- With
&dyn A
, you cannot create a new trait object reference&dyn B
, even though&dyn A
implementsB
. - With
Box<dyn A>
, you can freely create a new trait object reference&dyn B
.
Why This Happens
The explanation boils down to two fundamental aspects of Rust's type system:
1. Reference Coercion Rules
&dyn A
is already a borrowed reference (a fat pointer containing a data pointer and vtable). Rust intentionally prevents coercing one kind of reference into another, even when it might be technically safe. This is part of Rust's conservative approach to type safety.
2. Concrete Types vs References
Box<dyn A>
is a concrete type, not a reference. When you have a concrete type that implements a trait, Rust allows you to create any kind of trait object reference to it. This is similar to how you can create multiple different trait object references to any concrete type that implements multiple traits.
Code Examples in Practice
Here's how this distinction plays out in practical code:
// Working with concrete types - both ways work
let concrete = Concrete;
let a_ref: &dyn A = &concrete;
let b_ref: &dyn B = &concrete; // Works fine
// Working with trait objects
let a_trait: &dyn A = &concrete;
// let b_trait: &dyn B = a_trait; // Fails! Can't coerce references
// Working with Box
let boxed: Box<dyn A> = Box::new(concrete);
let b_trait: &dyn B = &boxed; // Works fine!
Best Practices and Implications
This behavior leads to some practical guidelines:
If you need to view a type through multiple trait objects, either:
- Use the concrete type and create references as needed
- Use
Box<dyn Trait>
when you need owned trait objects - Avoid trying to convert between different trait object references
When designing APIs:
- Consider using
Box<dyn Trait>
if clients need to view the object through multiple traits - Use
&dyn Trait
when you only need a single trait view and want to avoid allocation
Conclusion
This distinction between &dyn Trait
and Box<dyn Trait>
perfectly exemplifies Rust's philosophy:
- Safety First: Rust prevents reference coercion between trait objects to maintain its safety guarantees.
- Explicit over Implicit: When you need to view an object through multiple traits, Rust prefers explicit handling through concrete types.
- Zero-Cost Abstractions: Both mechanisms provide dynamic dispatch while maintaining different safety and usage characteristics.
Understanding these differences helps write more idiomatic Rust code and make better decisions about trait object usage in your APIs.
Remember: when you need flexibility in trait object conversions, reach for Box<dyn Trait>
. When you just need a simple borrowed view of a trait, &dyn Trait
is your friend. Each has its place in Rust's rich type system.
✨ This blog was written by AI! 🤖