Traits
Traits are Rust’s principal mechanism for ad-hoc polymorphism. A trait declares a set of methods that any implementing type provides; functions may be generic over types that implement a particular trait. The mechanism is conceptually similar to type classes in Haskell, interfaces in Java, and concepts in C++20 — with substantial differences. Rust’s traits admit static dispatch (via generics) by default; dynamic dispatch (via trait objects) is opt-in. Traits are the foundation of Rust’s standard library: Iterator, Display, Debug, Clone, Send, Sync are all traits, and the conventional Rust patterns rely heavily on trait-based design.
This page covers trait declarations, implementations, default methods, trait objects, supertraits, marker traits, the orphan rule, and the conventional traits in the standard library. The relationship to generics is in Generics.
Trait declarations
A trait declares a set of methods:
trait Greet {
fn greet(&self) -> String;
}
The declaration introduces a trait — a contract that any implementing type must satisfy. Methods may have default implementations:
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String { // default implementation
format!("Hello, {}!", self.name())
}
}
A type that implements Greet need only provide name; the default greet works automatically. Implementations may override the default.
Implementations
The impl Trait for Type form provides an implementation:
struct Dog {
name: String,
}
impl Greet for Dog {
fn name(&self) -> String {
self.name.clone()
}
// greet uses the default
}
let d = Dog { name: String::from("Rex") };
println!("{}", d.greet()); // "Hello, Rex!"
The impl Trait for Type syntax associates the trait’s methods with the type. The implementation may add the trait’s methods to the type’s API; users of the type can call the trait’s methods through the type’s value.
Multiple traits may be implemented for the same type:
impl Greet for Dog { /* ... */ }
impl Display for Dog { /* ... */ }
impl Debug for Dog { /* ... */ }
Each trait’s methods are accessible through the type.
The conventional standard traits
The Rust standard library defines several pervasive traits:
Display and Debug
For string conversion:
use std::fmt;
struct Point { x: f64, y: f64 }
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl fmt::Debug for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
}
}
let p = Point { x: 3.0, y: 4.0 };
println!("{}", p); // Display: "(3, 4)"
println!("{:?}", p); // Debug: "Point { x: 3, y: 4 }"
Debug is conventionally derived; Display is conventionally implemented manually when the type has a natural string representation.
Clone and Copy
For duplicating values:
#[derive(Clone, Copy)]
struct Point { x: f64, y: f64 }
let p = Point { x: 3.0, y: 4.0 };
let q = p; // copy (because Copy)
let r = p.clone(); // explicit clone
Copy is for trivial-to-duplicate types (no resource ownership); Clone is for explicit duplication. Copy requires Clone. Treated in Ownership and borrowing.
PartialEq and Eq
For equality:
#[derive(PartialEq, Eq)]
struct Point { x: i32, y: i32 }
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
assert!(a == b); // PartialEq
PartialEq admits the == operator; Eq (which extends PartialEq) marks the equality as reflexive — a == a is always true. Floating-point types implement PartialEq but not Eq (because of NaN).
PartialOrd and Ord
For ordering:
#[derive(PartialOrd, Ord, PartialEq, Eq)]
struct Version { major: u32, minor: u32, patch: u32 }
let a = Version { major: 1, minor: 0, patch: 0 };
let b = Version { major: 1, minor: 1, patch: 0 };
assert!(a < b); // PartialOrd
PartialOrd admits <, <=, >, >=; Ord requires a total ordering (every pair of values is comparable).
Default
For default values:
#[derive(Default)]
struct Config {
name: String, // defaults to ""
count: i32, // defaults to 0
enabled: bool, // defaults to false
}
let c = Config::default();
let c = Config { count: 10, ..Default::default() };
The Default trait admits constructing a default value; the ..Default::default() admits the conventional builder-style construction.
From and Into
For type conversion:
struct Wrapper(String);
impl From<&str> for Wrapper {
fn from(s: &str) -> Self {
Wrapper(String::from(s))
}
}
let w: Wrapper = "hello".into(); // uses Into (auto-derived from From)
let w = Wrapper::from("hello"); // direct call
Implementing From<X> for Y automatically provides Into<Y> for X. The conventional Rust pattern is to implement From, then call sites use either From::from or .into() (whichever reads better).
For fallible conversions, TryFrom:
impl TryFrom<i64> for u8 {
type Error = std::num::TryFromIntError;
fn try_from(value: i64) -> Result<Self, Self::Error> {
// ...
}
}
let n: Result<u8, _> = 257_i64.try_into(); // Err(...)
Iterator
For iteration:
struct Counter { value: u32 }
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.value < 10 {
self.value += 1;
Some(self.value)
} else {
None
}
}
}
let c = Counter { value: 0 };
for n in c {
println!("{}", n); // 1, 2, ..., 10
}
Treated in Iterators.
Drop
For cleanup:
struct Resource { name: String }
impl Drop for Resource {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
The Drop trait admits running cleanup when a value goes out of scope. Treated in Smart pointers.
Trait objects
Traits admit dynamic dispatch through trait objects: dyn Trait:
trait Animal {
fn speak(&self) -> String;
}
struct Dog;
impl Animal for Dog {
fn speak(&self) -> String { String::from("woof") }
}
struct Cat;
impl Animal for Cat {
fn speak(&self) -> String { String::from("meow") }
}
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.speak()); // dynamic dispatch
}
The Box<dyn Animal> is a trait object — a pointer to a value implementing Animal, plus a vtable (virtual function table) for dispatch.
The form admits heterogeneous collections; Vec<Box<dyn Animal>> may contain Dogs, Cats, and any other Animal implementer.
The cost: dynamic dispatch (virtual call through the vtable) and heap allocation for the box. The conventional Rust style favours static dispatch (generics) when the type can be known at compile time.
Object-safety
A trait must be object-safe to admit trait objects. The principal restrictions:
- No methods with generic type parameters.
- No methods returning
Self. - No
Self-typed parameters (other than the receiver).
trait NotObjectSafe {
fn make() -> Self; // not object-safe
}
trait ObjectSafe {
fn name(&self) -> String; // OK
fn process(&self, input: &str); // OK
}
For traits used principally with generics, object-safety is rarely a concern; for traits used with trait objects, the restriction must be respected.
The conventional pattern for “object-safe trait + non-object-safe extension” is splitting — a base object-safe trait plus a non-object-safe extension trait.
Generic functions with trait bounds
A generic function may constrain its type parameters with trait bounds:
fn print_all<T: Display>(items: &[T]) {
for item in items {
println!("{}", item);
}
}
print_all(&[1, 2, 3]); // i32 implements Display
print_all(&[String::from("a"), String::from("b")]);
The bound T: Display reads “T must implement Display”; the function admits any type that does. Multiple bounds with +:
fn show<T: Display + Clone>(item: T) {
let copy = item.clone();
println!("{}", copy);
}
Or with where clauses for clarity:
fn complex<T, U>(t: T, u: U) -> Result<T, U>
where
T: Display + Clone,
U: Debug,
{
// ...
}
The where form is the conventional choice for non-trivial bounds; it admits multi-line readable signatures.
impl Trait
The impl Trait syntax admits anonymous trait-bounded types:
fn make_iter() -> impl Iterator<Item = i32> {
(0..10).map(|n| n * 2)
}
let it = make_iter();
for n in it {
println!("{}", n);
}
The return type “implements Iterator” without specifying a concrete type. The mechanism admits returning closures, iterator adapters, and other unnameable types.
In argument position, impl Trait is shorthand for a generic parameter:
fn print_all(items: impl IntoIterator<Item = i32>) {
for item in items {
println!("{}", item);
}
}
// equivalent to:
fn print_all<T: IntoIterator<Item = i32>>(items: T) { /* ... */ }
The two forms are semantically equivalent for parameters; in return position, impl Trait admits returning unnameable types that generic parameters cannot express.
Trait inheritance (supertraits)
A trait may require its implementers to also implement another trait:
trait Mammal {
fn legs(&self) -> u32;
}
trait Carnivore: Mammal { // requires Mammal
fn predator(&self) -> bool;
}
A type implementing Carnivore must also implement Mammal. Inside Carnivore, the methods of Mammal are accessible:
trait Logger: Display { // requires Display
fn log(&self) {
println!("LOG: {}", self); // uses Display
}
}
The supertrait constraint is a form of trait inheritance; treated similar to type-class superclasses in Haskell.
Associated types
A trait may declare associated types — types parameterised by the implementer:
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
fn len(&self) -> usize;
}
struct IntVec(Vec<i32>);
impl Container for IntVec {
type Item = i32;
fn get(&self, index: usize) -> Option<&i32> {
self.0.get(index)
}
fn len(&self) -> usize {
self.0.len()
}
}
Associated types admit a more constrained API than generic parameters; each type may have only one implementation per trait, and the associated type is fixed by the implementation.
The standard library uses associated types extensively:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
The Iterator::Item is the type yielded; Add::Output is the type of the addition’s result. The associated-type form admits using the trait without specifying the type parameter at every call.
Marker traits
Marker traits have no methods; they tag types with properties:
| Marker trait | Indicates |
|---|---|
Copy | Value can be trivially duplicated |
Send | Value can be transferred between threads |
Sync | &T can be transferred between threads |
Sized | Value has a size known at compile time |
Unpin | Value’s address may not be stable |
Some are auto-derived by the compiler:
struct Point(i32, i32);
// Point is automatically Send and Sync (if its fields are)
Others are explicitly opt-in via unsafe impl:
struct Cell<T>(T);
unsafe impl<T> Send for Cell<T> {} // unsafe assertion
The marker traits admit substantial type-level reasoning; they are the foundation of Rust’s concurrency safety.
The orphan rule
The orphan rule prevents a crate from implementing a foreign trait for a foreign type:
- A trait may be implemented for a type if the trait OR the type is defined in the current crate.
- A foreign trait for a foreign type cannot be implemented from a third crate.
// In your crate:
struct MyType;
impl Display for MyType { /* ... */ } // OK: MyType is local
impl MyTrait for String { /* ... */ } // OK: MyTrait is local
// impl Display for String { /* ... */ } // ERROR: both foreign
The rule admits coherence — every (trait, type) pair has at most one implementation. The conventional workaround for the foreign-trait-foreign-type case is the newtype pattern:
struct StringWrapper(String);
impl Display for StringWrapper { // OK: StringWrapper is local
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{}", self.0)
}
}
The wrapper admits implementing the foreign trait; users opt into the wrapper when they want the implementation.
Deriving traits
The #[derive(...)] attribute auto-implements certain traits:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
struct Point {
x: i32,
y: i32,
}
The conventionally-derivable traits:
Debug,Clone,Copy— for the conventional duplications and printing.PartialEq,Eq,PartialOrd,Ord,Hash— for equality, ordering, hashing.Default— for default values.
The derives produce structural implementations: PartialEq compares all fields; Hash hashes all fields; Default uses each field’s default.
For non-trivial logic, manual implementations are required.
dyn Trait vs impl Trait vs generics
Three principal forms for “any type implementing Trait”:
| Form | When |
|---|---|
T: Trait (generic parameter) | Static dispatch; one implementation per type |
impl Trait (return) | Static dispatch; the type is anonymous |
impl Trait (parameter) | Same as T: Trait; sugar for generics |
dyn Trait (trait object) | Dynamic dispatch; runtime polymorphism |
// Generic (static dispatch):
fn print<T: Display>(item: T) { println!("{}", item); }
// impl Trait parameter (same as generic):
fn print(item: impl Display) { println!("{}", item); }
// impl Trait return (anonymous):
fn make_iter() -> impl Iterator<Item = i32> { 0..10 }
// dyn Trait (dynamic dispatch):
fn process(items: &[Box<dyn Display>]) {
for item in items {
println!("{}", item);
}
}
The conventional choice:
- Generics — for code that should be specialised per type (most cases).
impl Traitin argument position — when the type doesn’t matter (one-off use).impl Traitin return position — when the return type is unnameable (closures, iterator chains).dyn Trait— for heterogeneous collections or when types are not known at compile time.
The static forms admit zero-cost abstractions; the dynamic form admits flexibility at runtime cost.
Common patterns
Trait for behaviour
trait Validator {
fn validate(&self, input: &str) -> bool;
}
struct EmailValidator;
impl Validator for EmailValidator {
fn validate(&self, input: &str) -> bool {
input.contains('@')
}
}
struct LengthValidator { min: usize, max: usize }
impl Validator for LengthValidator {
fn validate(&self, input: &str) -> bool {
let len = input.len();
len >= self.min && len <= self.max
}
}
fn check(v: &dyn Validator, input: &str) -> bool {
v.validate(input)
}
The pattern admits substituting different validation strategies through the same interface.
Trait with default implementation
trait Container {
fn len(&self) -> usize;
fn is_empty(&self) -> bool { // default
self.len() == 0
}
}
Implementers may override is_empty for performance; the default suffices for most.
Generic with multiple bounds
fn process<T>(item: T) -> String
where
T: Display + Clone + Debug,
{
let copy = item.clone();
println!("debug: {:?}", copy);
format!("{}", item)
}
The bounds admit using the trait’s methods within the function.
Newtype with delegated implementation
struct Email(String);
impl Display for Email {
fn fmt(&self, f: &mut Formatter) -> Result {
self.0.fmt(f) // delegate to inner String
}
}
The newtype admits implementing foreign traits for the wrapper.
Trait objects in heterogeneous storage
trait Drawable {
fn draw(&self);
}
struct Canvas {
items: Vec<Box<dyn Drawable>>,
}
impl Canvas {
fn add<T: Drawable + 'static>(&mut self, item: T) {
self.items.push(Box::new(item));
}
fn render(&self) {
for item in &self.items {
item.draw();
}
}
}
The Box<dyn Drawable> admits any drawable type; the 'static bound admits the box to outlive any local stack frame.
A note on the conventional discipline
The contemporary Rust trait advice:
- Default to generics — static dispatch is faster and more flexible.
- Use trait objects for heterogeneous collections and runtime polymorphism.
- Implement the conventional standard traits (
Debug,Clone,PartialEq,Default,From/Into) where the type admits them. - Use
derivefor the auto-derivable traits. - Use the orphan rule to your advantage — implement traits in your own crate; use newtype for foreign-trait-foreign-type.
- Prefer associated types over generic parameters on traits when each implementer has a single canonical type.
- Use marker traits to constrain generic parameters to types with specific properties.
The combination — traits for ad-hoc polymorphism, generics with trait bounds, trait objects for dynamic dispatch — is the substance of Rust’s polymorphism story. The traits-based design admits code reuse and abstraction without the complexity of class hierarchies or the runtime cost of universal dynamic dispatch.