Rust Traits
debt(d5/e5/b7/t7)
Closest to 'specialist tool catches it' (d5). Many trait misuses (object-safety violations, orphan-rule errors, missing impl) are caught instantly by the compiler (d1), but the design-smell case from common_mistakes — reaching for Box<dyn Trait> when a generic bound would be zero-cost — is only flagged by clippy (detection_hints.tools: clippy) and the code_pattern Box<dyn \w+>. That specialist-tool layer is the binding case, so d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix ranges from trivial (add an `impl` block, add a `use`) to genuinely structural — switching Box<dyn Trait> dynamic dispatch to generic bounds ripples through function signatures and call sites, and wrapping foreign types in newtypes to satisfy the orphan rule touches every usage of that type. That spread of fixes lands at a one-component refactor.
Closest to 'strong gravitational pull' (b7). Traits define the polymorphism strategy across applies_to contexts (web, cli, queue-worker, library); the choice between static dispatch via bounds and dynamic dispatch via dyn shapes signatures, container types, and module boundaries everywhere. Trait design (object-safety, default methods, bounds) is load-bearing and hard to change later, pulling most API changes into its orbit.
Closest to 'serious trap' (t7). The misconception is explicit: developers from OOP assume traits work like class inheritance where types automatically gain base behavior. They contradict that model (impl is always explicit, no hierarchy, orphan rule, object-safety), so a competent dev from another language guesses wrong in a way that directly fights the borrow checker and compiler — a serious trap contradicting how inheritance works elsewhere.
Also Known As
TL;DR
Explanation
A trait in Rust is a named set of method signatures (and optional default implementations) that describes behavior a type can provide. Traits are Rust's primary tool for abstraction and polymorphism, filling the role that interfaces and abstract base classes play in other languages, but with important differences. A type gains a trait's behavior only by an explicit `impl Trait for Type` block; there is no inheritance hierarchy and no implicit `is-a` relationship. This makes capabilities composable: a single type can implement dozens of unrelated traits, and you add behavior by implementing more traits rather than extending a base class.
Traits enable two kinds of polymorphism. Static dispatch uses generics with trait bounds, such as `fn print<T: Display>(x: T)`, where the compiler monomorphizes a specialized copy per concrete type with zero runtime cost. Dynamic dispatch uses trait objects like `Box<dyn Display>` or `&dyn Display`, which store a pointer to data plus a vtable, resolving method calls at runtime in exchange for a small indirection cost and a heap-allocated or borrowed object. Choosing between them is a deliberate trade-off, not a default.
Default methods let a trait supply implementations that any implementor inherits unless it overrides them, which is the closest Rust gets to shared code reuse. Traits can also require other traits via supertraits (`trait Ord: PartialOrd`), declare associated types and constants, and be implemented generically with blanket impls like `impl<T: Display> ToString for T`.
The orphan rule constrains where you may write an impl: either the trait or the type must be defined in your crate. This prevents two crates from defining conflicting implementations for the same trait and type. The newtype pattern (wrapping a foreign type in a local struct) is the standard workaround when you need to implement a foreign trait for a foreign type.
Object safety governs which traits can become trait objects: methods returning `Self` or using generic type parameters generally break `dyn` compatibility. Understanding traits means understanding explicit implementation, static versus dynamic dispatch, default methods, the orphan rule, and object safety - none of which map cleanly onto classical inheritance.
Common Misconception
Why It Matters
Common Mistakes
- Assuming a type inherits a trait implicitly instead of writing an explicit impl Trait for Type block.
- Reaching for Box<dyn Trait> dynamic dispatch when a generic with a trait bound would be zero-cost.
- Trying to implement a foreign trait for a foreign type and hitting the orphan rule instead of using a newtype wrapper.
- Designing a trait with methods returning Self or generic parameters, then being unable to use it as a dyn trait object.
- Forgetting to bring a trait into scope with `use`, so its methods appear missing on a type that does implement it.
Avoid When
- You need to store a heterogeneous collection of different concrete types behind one interface, where a generic single-type bound cannot express it and dyn is the right tool.
- Adding a trait abstraction for a single concrete implementation that will never have a second one, which is premature generality.
- The behavior is genuinely data-specific and forcing it behind a shared trait would create a leaky or meaningless abstraction.
When To Use
- Defining shared behavior that several unrelated types should implement, such as serialization or comparison.
- Writing generic functions and containers that operate over any type satisfying a bound, getting zero-cost static dispatch.
- Providing default method implementations so implementors share code without an inheritance hierarchy.
- Decoupling a module from concrete types by programming against a trait for testability and substitution.
Code Examples
use std::fmt::Display;
// Forcing dynamic dispatch and heap allocation where generics would do.
struct Logger {
sinks: Vec<Box<dyn Display>>,
}
fn print_all(items: Vec<Box<dyn Display>>) {
for item in items {
// Runtime vtable indirection on every call, even when types are known.
println!("{}", item);
}
}
fn main() {
// Every value must be boxed on the heap just to satisfy the API.
let logger = Logger {
sinks: vec![Box::new(1), Box::new("hi")],
};
print_all(logger.sinks);
}
use std::fmt::Display;
// Static dispatch via a trait bound: zero-cost, monomorphized per type.
fn print_one<T: Display>(item: T) {
println!("{}", item);
}
// A default method gives shared behavior without inheritance.
trait Describe {
fn name(&self) -> String;
fn describe(&self) -> String {
format!("This is {}", self.name())
}
}
struct Widget;
// Behavior is opted into explicitly.
impl Describe for Widget {
fn name(&self) -> String {
"a widget".to_string()
}
}
fn main() {
print_one(1);
print_one("hi");
println!("{}", Widget.describe());
}