TraitsConcept
Explanation
A trait defines a set of behavior — methods a type promises to provide —
that any type can then implement, independent of that type's own
definition. It's Rust's answer to "shared behavior across unrelated
types," filling the role interfaces play in Java/C#, protocols in Swift,
or type classes in Haskell. For instance, a Greet trait declaring a
greet method can be implemented for a Cat struct with impl Greet for Cat.
Because traits are implemented separately from a type's definition (via
impl Trait for Type, potentially in a completely different module or
even a different crate than the type itself), a type can implement any
number of unrelated traits without any of them needing to know about each
other — this is the mechanism behind
decoupling code from
concrete types: a function can depend on "anything implementing this
trait" instead of a specific concrete type, without the trait author and
the type author ever needing to coordinate directly.
Traits are also how Rust achieves polymorphism without classical inheritance — see trait objects & dynamic dispatch for the runtime-polymorphism side, and static dispatch & monomorphization for the compile-time side.
Basic usage example
trait Greet {
fn greet(&self) -> String;
}
struct Cat;
impl Greet for Cat { // <- Cat implements the Greet trait
fn greet(&self) -> String { "meow".into() }
}
println!("{}", Cat.greet());
Best practices & deeper information
Implementing traits
A domain type gains broad interoperability by implementing a standard
trait like Display, rather than a bespoke method — any code already
written against Display accepts it for free.
struct Temperature { celsius: f64 }
impl std::fmt::Display for Temperature { // <- implementing a trait, not a one-off to_string() method
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:.1}°C", self.celsius)
}
}
fn log(value: &impl std::fmt::Display) { // <- accepts anything implementing Display
println!("reading: {value}");
}
log(&Temperature { celsius: 21.5 });
Why this way: eagerly implementing common standard traits lets a type slot directly into generic code, formatting macros, and collections that already know how to work with them, instead of every caller needing a type-specific method — the API Guidelines' C-COMMON-TRAITS codifies this.
Designing a public API
Shipping a trait as a library's extension point, instead of a concrete struct, is a deliberate design choice — callers implement the trait for their own types instead of being locked into the crate's.
pub trait Storage { // <- the crate's extension point is a trait, not a fixed struct
fn get(&self, key: &str) -> Option<String>;
fn set(&mut self, key: &str, value: String);
}
pub struct Cache<S: Storage> {
backend: S,
}
impl<S: Storage> Cache<S> {
pub fn refresh(&mut self, key: &str, value: String) {
self.backend.set(key, value);
}
}
Why this way: keeping the trait narrow (two methods, nothing about how storage is implemented) leaves room to add backends later without breaking existing implementers — see the API Guidelines on future-proofing for the broader case around designing traits as stable extension points.
Testing
Defining a trait around a dependency — even one with a single real implementation — opens the door to a test-only stand-in, without any conditional compilation inside the production code path.
trait Clock {
fn now(&self) -> u64;
}
struct SystemClock;
impl Clock for SystemClock {
fn now(&self) -> u64 { /* reads the real system time */ 1_700_000_000 }
}
struct FixedClock(u64); // <- test-only implementation of the same trait
impl Clock for FixedClock {
fn now(&self) -> u64 { self.0 }
}
fn is_expired(clock: &impl Clock, expiry: u64) -> bool {
clock.now() > expiry
}
assert!(is_expired(&FixedClock(2_000_000_000), 1_000_000_000)); // no real clock involved
Why this way: depending on the trait rather than SystemClock directly
means the test controls time deterministically instead of racing the real
clock — a standard use of trait seams for testability, as the
Rust Book's mock-object example
illustrates with the same trait-double approach.