In Rust, handling polymorphism and abstractions can significantly impact the design and flexibility of your application. Two common ways to implement trait abstractions are using Box<dyn Trait>
and impl Trait
. Although they might seem similar at first glance due to both dealing with traits, they serve different purposes and have some fundamental differences. Understanding these differences can help you make better decisions when designing your Rust applications.
What is Box<dyn Trait>
?
Box<dyn Trait>
is a type of trait object. Trait objects let you use dynamic dispatch to call methods on a trait, while the concrete type implementing the trait is not explicitly known at compile time. This is very useful when you need polymorphism and the specific type is determined at runtime.
trait Drawable {
fn draw(&self);
}
struct Circle;
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle");
}
}
fn draw_shape(shape: Box<dyn Drawable>) {
shape.draw();
}
fn main() {
let circle = Box::new(Circle);
draw_shape(circle);
}
Here, draw_shape
function can accept any type that implements Drawable
trait, but it requires boxing, i.e., wrapping into Box<dyn Drawable>
. This has the overhead of heap allocation, but it is flexible because the exact type need not be known at compile time.
What is impl Trait
?
The impl Trait
syntax was introduced to provide a way to specify that a function returns some type that implements a given trait without specifying the concrete type. This is also known as opaque types.
trait Flyable {
fn fly(&self);
}
struct Bird;
impl Flyable for Bird {
fn fly(&self) {
println!("Flying like a bird");
}
}
fn get_flyable() -> impl Flyable {
Bird
}
fn main() {
let flying_creature = get_flyable();
flying_creature.fly();
}
In the example above, get_flyable
returns some type that implements the Flyable
trait using impl Flyable
, without specifying what type it is outside the function. Here, the exact type does need to be consistent across all calls and known within the function returning it, maintaining the compile-time type safety and often optimizing away any dynamic dispatch overhead.
Differences Between Box<dyn Trait>
and impl Trait
- Type Erasure: With
Box<dyn Trait>
, the type is erased, meaning you don't need to know the concrete type, allowing runtime polymorphism. - Interface Stability:
Box<dyn Trait>
interfaces can evolve without impacting users who compile against a binary. The dispatch is dynamic here due to vtables. - Heap Allocation: Using
Box<dyn Trait>
, the object needs to be moved to the heap, which can incur performance costs due to allocation/deallocation. - Compile-time Type:
impl Trait
retains compile-time information of a concrete but opaque type, hence offering optimizations borrowed from knowing the concrete type. - Generic Functions:
impl Trait
is useful in defining return types for generic functions without including explicit type parameters.
When to Use Which
If your use case requires passing potentially self-referential structured data or requires storing heterogeneous collections of types that implement the same trait, Box<dyn Trait>
can be very valuable. In contrast, use impl Trait
when you want to leverage zero-cost abstractions and maintain type information as far as possible while coding, making the static dispatch.
Choosing between Box<dyn Trait>
and impl Trait
fundamentally revolves around your specific needs for dynamic versus static dispatch and explicit versus implicit type management in your Rust programs.