Rust's Trait System as Interface Design
A trait describes what a type can do, not what a type is. Display means "this type knows how to format itself for a user"; Iterator means "this type knows how to produce a sequence of values one at a time." Any type, anywhere, can implement a trait, which makes traits closer to a capability contract than to a class hierarchy.
This distinction shapes how Rust does the thing most languages call "interfaces." Instead of one mechanism, Rust offers two ways to use a trait once it exists: generics with monomorphization, resolved entirely at compile time, and trait objects with dynamic dispatch, resolved at runtime. They look similar on the page and behave very differently underneath, and picking between them is one of the more consequential API design decisions in a Rust codebase.
This page is the conceptual anchor for the section. Traits Basics covers the syntax hands-on; this page explains what a trait fundamentally is and how the two dispatch strategies actually work.
Summary
- A trait is a capability contract - a set of methods a type promises to implement - and Rust offers two distinct ways to program against that contract: compile-time generics or runtime trait objects.
- Insight: The choice between generics and trait objects changes your program's performance profile, binary size, and API flexibility; picking the wrong one for a given situation costs either unnecessary runtime overhead or unnecessary compile-time rigidity.
- Key Concepts: trait, trait bound, monomorphization, static dispatch, trait object, dynamic dispatch.
- When to Use This Model: Deciding whether a function parameter should be
T: Trait/impl Traitor&dyn Trait, designing plugin-style APIs, and understanding why some traits cannot become trait objects at all. - Limitations/Trade-offs: Generics can bloat binary size with duplicated code per concrete type; trait objects pay a small runtime indirection cost and cannot use every trait (object safety rules apply).
- Related Topics: generics, static versus dynamic dispatch, trait objects, associated types.
Foundations
A trait is declared once and can be implemented by any number of unrelated types, in any crate that has visibility into both the trait and the type (subject to the orphan rule). This is fundamentally different from class-based inheritance, where a method comes from a single fixed position in a hierarchy - a trait is a promise a type opts into, not a slot it inherits into.
trait Summarize {
fn summary(&self) -> String;
}String, a custom Article struct, and a third-party Tweet type could all implement Summarize independently, without any of them knowing about each other or sharing a common ancestor. What unites them is not what they are, it is that they all can produce a summary - a capability, not a category.
Once a trait exists, you can write code that only needs to know a type has that capability, without caring which concrete type it actually is. This is the essence of a trait bound: fn describe<T: Summarize>(item: T) says "give me anything that can summarize itself," and the function body can call .summary() without knowing or caring whether T turns out to be an Article or a Tweet.
Mechanics & Interactions
How that trait bound gets turned into a running program is where the two dispatch strategies diverge, and the difference is not cosmetic.
Generics with a trait bound are resolved through monomorphization: for every distinct concrete type used with a generic function, the compiler generates a separate, specialized copy of that function at compile time. describe::<Article> and describe::<Tweet> become two entirely different functions in the compiled binary, each with .summary() calls resolved directly to the right implementation, no runtime lookup involved. This is static dispatch - the compiler knows exactly which function to call before the program ever runs.
trait Summarize { fn summary(&self) -> String; }
struct Article(String);
impl Summarize for Article {
fn summary(&self) -> String { format!("Article: {}", self.0) }
}
fn describe<T: Summarize>(item: &T) -> String {
item.summary() // resolved at compile time, per concrete T
}Trait objects take the opposite approach. &dyn Summarize or Box<dyn Summarize> erases the concrete type entirely, storing instead a pointer to the data plus a pointer to a vtable - a table of function pointers for that specific type's implementation of the trait. Calling .summary() through a trait object means looking up the right function pointer in the vtable at runtime, then jumping to it. This is dynamic dispatch, and it is what lets a single Vec<Box<dyn Summarize>> hold an Article, a Tweet, and any other Summarize implementer side by side, something generics alone cannot do because a generic function is specialized to one concrete type per call.
That last point is the practical fork in the road: generics require the concrete type to be knowable and fixed per call site (or duplicated per type used), while trait objects allow a genuinely heterogeneous collection or a function boundary where the caller's concrete type shouldn't even need to be named. Not every trait can become a trait object, though - a trait is only object-safe if its methods do not require knowing Self's concrete size (no Self return types, no generic methods on the trait itself), because a vtable has no way to represent "a method that returns whatever concrete type I actually am."
Advanced Considerations & Applications
Both dispatch strategies are legitimate defaults for different problems, and production Rust code uses both, often in the same crate.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Generics + static dispatch | Zero runtime overhead; compiler can inline and optimize per type | Binary size grows with each concrete type instantiated; concrete type must be known at each call site | Hot paths, library APIs where callers supply one clear type |
| Trait objects + dynamic dispatch | Heterogeneous collections; smaller binary; type can vary at runtime | Small per-call indirection cost; not every trait is object-safe | Plugin systems, UI widget trees, anywhere the set of concrete types isn't fixed at compile time |
impl Trait return type | Hides a concrete type from callers without heap allocation | Only one concrete type per function body - can't conditionally return two different types | Iterator-returning functions, builder-style APIs |
| Duck typing (dynamic languages) | No trait declaration needed at all | No compile-time contract; errors surface only when a missing method is actually called | Rapid prototyping outside Rust's type system entirely |
The binary-size trade-off with generics is real and sometimes surprising: a heavily generic library used with dozens of concrete types can produce noticeably larger binaries than the equivalent trait-object-based design, because monomorphization is quite literally copying the function body once per type. Large Rust projects sometimes deliberately introduce a dyn Trait-based boundary specifically to stop this duplication from propagating outward, accepting the small dispatch cost in exchange for a smaller, more predictable binary.
The performance difference between the two, in absolute terms, is frequently smaller than developers assume - a vtable lookup is a single indirect jump, not a meaningful cost in most application code. The decision is better framed around API shape than raw speed: does this boundary need to accept or store a genuinely unknown, possibly-growing set of concrete types (reach for dyn Trait), or is the type always resolvable per call site and performance-sensitive (reach for generics)? impl Trait in argument or return position is frequently the right middle ground when you want static dispatch's speed without spelling out a generic parameter the caller never needs to see.
Associated types add a further axis to this design space: a trait like Iterator uses type Item to let each implementer fix one output type without turning that type into a generic parameter of the trait itself, which keeps call sites simpler at the cost of one implementation per concrete Item type rather than a family of implementations parameterized over it.
Common Misconceptions
- "Traits are just Rust's version of interfaces from Java or C#." They are close, but a trait can be implemented for types you don't own (subject to the orphan rule) and can be used generically without ever being boxed - Java/C# interfaces are always used through a reference, which is closer to Rust's
dyn Traitalone. - "Generics and trait objects are two syntaxes for the same thing." They compile to fundamentally different code - one duplicates the function per type at compile time, the other keeps one function and looks up behavior through a vtable at runtime - with real, different performance and flexibility consequences.
- "
dyn Traitis always slower and should be avoided." The dispatch overhead is a single indirect call, often negligible next to the actual work being done; trait objects are the correct choice whenever the set of concrete types genuinely varies at runtime. - "Any trait can be turned into a trait object with
dyn." Object safety is a real constraint - traits with generic methods or methods returningSelfcannot become trait objects, because a vtable cannot represent a method whose signature depends on an erased concrete type. - "Monomorphization means my generic code is compiled once and reused for every type." It is the opposite - a separate copy is generated per concrete type actually used, which is what makes static dispatch fast but is also what grows binary size.
FAQs
What is a trait, in one sentence?
A set of methods a type promises to implement, describing a capability the type has rather than a category it belongs to, implementable by any type with visibility into both the trait and the type.
How is a trait different from a class-based interface?
A trait can be implemented for a type after the fact, including types you don't own (within the orphan rule), and doesn't require the implementing type to sit anywhere in a hierarchy - it's an opt-in capability rather than an inherited slot.
What actually happens during monomorphization?
For every distinct concrete type used with a generic function or type, the compiler emits a separate specialized copy of that code at compile time, with all trait method calls resolved directly rather than looked up at runtime.
How does a trait object know which implementation to call?
It stores a pointer to the data alongside a pointer to a vtable - a table of function pointers specific to that concrete type's trait implementation - and looks up the right function pointer at the call site during execution.
Why can't every trait become a `dyn Trait` object?
A trait object's vtable has to represent every method with a fixed signature, so methods that depend on the concrete Self type (generic methods, or methods returning Self) can't be represented - this is called object safety, and it's checked by the compiler.
Is `dyn Trait` slower than generics in practice?
There is a small, real cost - one indirect function call per invocation through the vtable - but it is rarely the bottleneck in application code. The bigger practical difference is usually binary size and API flexibility, not raw speed.
When should I choose trait objects over generics?
When the set of concrete types isn't fixed at compile time - a plugin system, a heterogeneous collection, or a function boundary where you deliberately don't want callers to need to know or name the concrete type.
When should I choose generics over trait objects?
When the concrete type is known (or resolvable) at each call site and performance matters, since static dispatch lets the compiler inline and optimize per type with no runtime lookup at all.
What's the difference between `impl Trait` and `dyn Trait`?
impl Trait still uses static dispatch - it hides the concrete type from the caller's signature but the compiler knows exactly which type it is at compile time; dyn Trait erases the type and uses dynamic dispatch via a vtable at runtime.
Why do generics sometimes bloat binary size?
Because monomorphization generates one full copy of the generic function's code per concrete type it's used with - ten types used with the same generic function can mean ten near-identical copies of that function in the final binary.
What are associated types for, and how do they relate to generics?
An associated type (like Iterator::Item) lets a trait implementer fix one output type without making it a generic parameter of every use of the trait, which keeps call sites simpler at the cost of one implementation per concrete associated type rather than a family parameterized over it.
Can a single codebase use both generics and trait objects for the same trait?
Yes, and it's common - a library might expose a generic function for the hot path where types are known and performance matters, while also accepting Box<dyn Trait> at a boundary (like a plugin registry) where the set of types genuinely varies at runtime.
Related
- Traits Basics - defining and implementing traits hands-on
- Generics - the syntax and mechanics of generic parameters
- Static vs Dynamic Dispatch - a deeper comparison of the two strategies
- Trait Objects & Dynamic Dispatch -
dyn Traitmechanics and object safety in depth - Trait Bounds - constraining generic parameters with traits
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.