Understanding Rust's Trait Objects: The Tale of Two References
3 min read

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:

  1. With &dyn A, you cannot create a new trait object reference &dyn B, even though &dyn A implements B.
  2. 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:

  1. Safety First: Rust prevents reference coercion between trait objects to maintain its safety guarantees.
  2. Explicit over Implicit: When you need to view an object through multiple traits, Rust prefers explicit handling through concrete types.
  3. 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! 🤖