A trait defines behaviour a type can have, and types opt into it by implementing it. If you are coming from an object-oriented language the closest analogy is an interface, but traits do things interfaces cannot: you can implement your own trait for types you did not write, and you can supply default method bodies.
Traits are how Rust does polymorphism without inheritance, and the design has consequences that are not obvious from the syntax. The two that matter most in practice are the choice between static and dynamic dispatch, and the orphan rule that decides what you are allowed to implement.
Table of contents
- Defining and implementing a trait
- Traits as bounds on generics
- Static versus dynamic dispatch
- The orphan rule, and how to work around it
- Derive, and the traits worth knowing
- How this fits the rest of the stack
- FAQ
Defining and implementing a trait
A trait is a set of method signatures. Any type that implements it must provide those methods.
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Article {
pub headline: String,
pub author: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}
Traits can also provide default implementations, which implementors may accept or override. This is the part interfaces in many languages lack, and it removes a great deal of repetition.
pub trait Summary {
fn summarize_author(&self) -> String;
// Default body, expressed in terms of the required method.
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
impl Summary for Article {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
// summarize() comes for free.
}
A default method can call required methods on the same trait, which lets you define a small required surface and a large derived one. Iterator is the canonical example: implement next, and you get map, filter, fold, and dozens more.
Traits as bounds on generics
The main use of a trait is constraining a generic parameter, which is how a function says it accepts any type that can do a particular thing.
// These three are the same function, written three ways.
pub fn notify(item: &impl Summary) {
println!("Breaking! {}", item.summarize());
}
pub fn notify_generic<T: Summary>(item: &T) {
println!("Breaking! {}", item.summarize());
}
pub fn notify_where<T>(item: &T)
where
T: Summary,
{
println!("Breaking! {}", item.summarize());
}
// Multiple bounds.
fn process<T: Summary + Clone + std::fmt::Debug>(item: T) { }
The impl Trait form in argument position is the most readable for simple cases. The explicit generic form is needed when two arguments must be the same type, since impl Summary twice permits two different types. The where clause is for when the bounds get long enough to hurt the signature.
Crucially, this is resolved at compile time. The compiler generates a separate specialised copy of the function for every concrete type it is called with, a process called monomorphisation. There is no runtime lookup and the calls can be inlined, so a generic function costs exactly what a hand-written one would.
The cost is compile time and binary size. Ten types means ten copies.
Static versus dynamic dispatch
Generics give you one type per call site. Sometimes you need a collection holding several different types that all implement the same trait, and for that you need a trait object.
// Static dispatch: every element must be the same concrete type.
fn print_all<T: Summary>(items: &[T]) {
for item in items {
println!("{}", item.summarize());
}
}
// Dynamic dispatch: a mixed collection, resolved at runtime.
fn print_mixed(items: &[Box<dyn Summary>]) {
for item in items {
println!("{}", item.summarize());
}
}
let items: Vec<Box<dyn Summary>> = vec![
Box::new(article),
Box::new(tweet), // a different type, same trait
];
The dyn keyword marks a trait object. It is a fat pointer: one half points at the data, the other at a vtable of function pointers for that type’s implementation. Method calls go through the vtable, so they cannot be inlined.
The choice in practice: default to generics, and reach for dyn when you genuinely need heterogeneity, when monomorphisation is bloating compile times, or when the type is not known until runtime, such as a plugin loaded from configuration.
Not every trait can be a trait object. To be object safe, a trait must not have generic methods and must not return Self, because the compiler needs a single vtable layout and neither of those has one. This is why some traits work fine as bounds but produce an error the moment you write dyn.
The orphan rule, and how to work around it
You can implement a trait for a type if you own the trait, or you own the type, or both. You cannot implement someone else’s trait for someone else’s type.
// Fine: your trait, a foreign type.
impl Summary for String { }
// Fine: a foreign trait, your type.
impl std::fmt::Display for Article { }
// Not allowed: both foreign.
// impl std::fmt::Display for Vec<String> { }
This is coherence, and it exists so that two different crates cannot each provide a conflicting implementation of the same trait for the same type, leaving the compiler to arbitrate. The restriction is genuinely necessary, and it is also the thing that most often blocks what you were about to write.
The standard workaround is the newtype pattern: wrap the foreign type in a thin struct you own, which makes the type local and the implementation legal.
struct Wrapper(Vec<String>);
impl std::fmt::Display for Wrapper {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
The wrapper costs nothing at runtime, since a single-field struct has the same layout as its field. What it costs is ergonomics: the inner type’s methods are not available on the wrapper unless you forward them or implement Deref.
Derive, and the traits worth knowing
Many common traits can be implemented mechanically, and the derive attribute does it for you.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Config {
name: String,
retries: u32,
}
The traits that come up constantly:
- Debug: formatting with {:?}, and effectively mandatory on any public type.
- Clone and Copy: explicit duplication, and implicit bitwise copy for small plain-data types.
- PartialEq and Eq: equality comparison, needed for == and for use as a HashMap key.
- PartialOrd and Ord: ordering, needed for sorting.
- Default: a sensible zero value, and the basis of the struct update syntax.
- From and Into: conversions. Implement From and you get Into free.
- Display: user-facing formatting with {}. Cannot be derived, since only you know how it should read.
The From trait is worth singling out, because implementing it for your error type is what makes the question mark operator convert errors automatically as they propagate. That single trait is most of what makes Rust error handling pleasant rather than tedious.
How this fits the rest of the stack
Rust services get deployed the same way any other service does: a container built from the repository, environment variables that are not in source control, and logs when a release goes wrong. Rust is not on the list of language runtimes with a dedicated build path on RunxBuild, so a Rust service deploys as a Docker container, which is a first-class option rather than a workaround. The RunxBuild hosting calculator shows the service, managed database, and storage as separate line items.
Useful related references:
- How to Make a Function Public in Rust: pub and the Four Levels Between
- Rust Redis: The redis-rs Crate, the Connection Pool, the Async Story, and the One Mistake That Blocks the Event Loop
- Services on RunxBuild
FAQ
What is a trait in Rust?
A trait defines behaviour that types can implement, similar to an interface in other languages. Unlike most interfaces it can supply default method bodies, and you can implement your own trait for types you did not define.
What is the difference between impl Trait and dyn Trait?
impl Trait is static dispatch: the compiler generates a specialised copy per concrete type, with no runtime lookup. dyn Trait is dynamic dispatch through a vtable, which allows a collection of mixed types at the cost of an indirect call.
Why can I not implement a trait for a type?
The orphan rule requires that either the trait or the type is local to your crate. It prevents conflicting implementations from different crates. Wrap the foreign type in a newtype struct you own to work around it.
What does object safe mean in Rust?
A trait is object safe if it can be used as dyn Trait. It must have no generic methods and must not return Self, because the compiler needs a single vtable layout that all implementors share.
Should I use generics or trait objects?
Default to generics for performance and inlining. Use trait objects when you need a collection of different types behind one trait, when monomorphisation is hurting compile times, or when the type is decided at runtime.