Rust Notes: Traits
A review of traits in Rust.
Rust Traits — A Practical Guide
Traits are one of the most important features in Rust.
They are used to define shared behavior, provide polymorphism, constrain generic types, enable static and dynamic dispatch, and define interfaces between parts of a program.
If you come from C++ or Java, traits can feel similar to a combination of:
- interfaces
- abstract classes
- generic constraints
- type classes
But Rust’s model is different because it does not use traditional class inheritance.
A useful mental model is:
Structs define what data a type has. Traits define what a type can do.
1. Defining a Trait
The simplest trait looks like this:
1
2
3
trait Animal {
fn speak(&self);
}
This says:
Any type that implements
Animalmust provide aspeakmethod.
The trait itself does not define a concrete Animal object.
It defines behavior that other types can implement.
1
2
3
4
5
6
7
8
Animal
trait
│
┌───────┴───────┐
▼ ▼
Dog Cat
│ │
speak() speak()
2. Implementing a Trait
Suppose we have two structs:
1
2
3
struct Dog;
struct Cat;
We can implement the trait for each:
1
2
3
4
5
6
7
8
9
10
11
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}
Now both types implement Animal.
1
2
3
4
5
let dog = Dog;
let cat = Cat;
dog.speak();
cat.speak();
The syntax is:
1
2
3
impl TraitName for TypeName {
// trait implementation
}
3. Traits Are Not Inheritance
Rust does not have traditional class inheritance.
You cannot write:
1
2
struct Dog : Animal {
}
Instead, Rust uses traits:
1
2
3
4
5
6
7
8
9
10
11
trait Animal {
fn speak(&self);
}
struct Dog;
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
The relationship is:
1
2
3
4
5
Animal
↑
│ implements
│
Dog
not:
1
2
3
4
5
Animal
↑
│ inherits
│
Dog
This distinction is important.
Rust generally favors:
Composition + traits instead of inheritance.
4. Trait Methods
A trait can contain methods.
1
2
3
4
5
trait Animal {
fn speak(&self);
fn name(&self) -> &str;
}
A type implementing the trait must implement both:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Dog {
name: String,
}
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
fn name(&self) -> &str {
&self.name
}
}
5. &self, &mut self, and self
Trait methods can use the same receiver forms as normal methods.
Immutable reference
1
2
3
trait Animal {
fn speak(&self);
}
The method can read the object but cannot mutate it.
Mutable reference
1
2
3
trait Animal {
fn rename(&mut self, name: String);
}
The method can mutate the object.
Taking ownership
1
2
3
trait Animal {
fn destroy(self);
}
Calling the method consumes the object.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
trait Animal {
fn consume(self);
}
struct Dog;
impl Animal for Dog {
fn consume(self) {
println!("Dog consumed");
}
}
let dog = Dog;
dog.consume();
// dog can no longer be used
6. Associated Functions
A trait can define a function without self.
1
2
3
trait Animal {
fn create() -> Self;
}
An implementation can provide it:
1
2
3
4
5
6
7
struct Dog;
impl Animal for Dog {
fn create() -> Self {
Dog
}
}
Call it using the implementing type:
1
let dog = Dog::create();
This is similar to an associated function such as:
1
String::new()
7. Default Trait Methods
Traits can provide default implementations.
1
2
3
4
5
trait Animal {
fn speak(&self) {
println!("Some animal sound");
}
}
Now a type can implement the trait without implementing speak:
1
2
3
struct Dog;
impl Animal for Dog {}
The default implementation is used:
1
2
let dog = Dog;
dog.speak();
A type can also override the default:
1
2
3
4
5
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
This is useful when many types share the same behavior.
8. Traits Can Have Multiple Methods
For example:
1
2
3
4
5
6
7
trait Animal {
fn name(&self) -> &str;
fn speak(&self);
fn age(&self) -> u32;
}
An implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Dog {
name: String,
age: u32,
}
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn speak(&self) {
println!("Woof!");
}
fn age(&self) -> u32 {
self.age
}
}
9. Trait Bounds
One of the most important uses of traits is constraining generic types.
Suppose:
1
2
3
fn print_animal<T>(animal: &T) {
animal.speak();
}
This does not compile because Rust does not know that T has a speak method.
We need a trait bound:
1
2
3
fn print_animal<T: Animal>(animal: &T) {
animal.speak();
}
Now Rust knows:
Tmust implementAnimal.
The syntax:
1
T: Trait
means:
TimplementsTrait.
10. Generic Trait Bounds with where
The same function can be written as:
1
2
3
4
5
6
fn print_animal<T>(animal: &T)
where
T: Animal,
{
animal.speak();
}
Both forms mean the same thing:
1
fn print_animal<T: Animal>(animal: &T)
and:
1
2
3
fn print_animal<T>(animal: &T)
where
T: Animal,
The where syntax becomes more useful when constraints become complicated.
For example:
1
2
3
4
5
6
7
fn process<T, U>(value: &T, other: &U)
where
T: Animal + Clone,
U: Display + Debug,
{
// ...
}
11. Multiple Trait Bounds
A type can be required to implement multiple traits.
1
2
3
fn process<T: Animal + Clone>(animal: &T) {
// ...
}
This means:
1
2
3
T must implement Animal
AND
T must implement Clone
Equivalent where syntax:
1
2
3
4
5
fn process<T>(animal: &T)
where
T: Animal + Clone,
{
}
12. impl Trait in Function Parameters
Instead of explicitly naming a generic type:
1
2
3
fn speak<T: Animal>(animal: &T) {
animal.speak();
}
you can write:
1
2
3
fn speak(animal: &impl Animal) {
animal.speak();
}
This means approximately:
Accept some type that implements
Animal.
For simple cases, these are very similar:
1
fn speak<T: Animal>(animal: &T)
and:
1
fn speak(animal: &impl Animal)
The generic form becomes more useful when the same type parameter appears multiple times.
13. Multiple impl Trait Parameters
Consider:
1
2
fn compare(a: &impl Animal, b: &impl Animal) {
}
This does not necessarily mean a and b have the same concrete type.
They could be:
1
2
a → Dog
b → Cat
If you want them to have the same type:
1
2
fn compare<T: Animal>(a: &T, b: &T) {
}
Now:
1
2
3
4
T = Dog
a → Dog
b → Dog
This distinction is important.
14. impl Trait as a Return Type
Rust can return a type implementing a trait without exposing its concrete type.
1
2
3
fn create_animal() -> impl Animal {
Dog
}
The caller knows:
The returned value implements
Animal.
But the caller does not need to know the exact concrete type.
This is especially useful for iterators and complex types.
For example:
1
2
3
fn numbers() -> impl Iterator<Item = i32> {
0..10
}
The concrete iterator type can be complicated, so impl Iterator hides it.
15. impl Trait Does Not Mean Any Type
This:
1
2
3
fn create_animal() -> impl Animal {
Dog
}
does not mean the function can sometimes return a Dog and sometimes a Cat.
This would fail:
1
2
3
4
5
6
7
fn create_animal(condition: bool) -> impl Animal {
if condition {
Dog
} else {
Cat
}
}
because impl Trait represents one concrete hidden type for that function.
If you need different concrete types at runtime, use a trait object:
1
2
3
4
5
6
7
fn create_animal(condition: bool) -> Box<dyn Animal> {
if condition {
Box::new(Dog)
} else {
Box::new(Cat)
}
}
16. Static Dispatch
Generic trait bounds normally use static dispatch.
1
2
3
fn speak<T: Animal>(animal: &T) {
animal.speak();
}
If we call:
1
2
3
4
5
let dog = Dog;
let cat = Cat;
speak(&dog);
speak(&cat);
the compiler knows the concrete type.
Conceptually:
1
2
3
4
5
6
7
8
9
speak::<Dog>()
│
▼
Dog::speak()
speak::<Cat>()
│
▼
Cat::speak()
The decision is made at compile time.
Advantages include:
- no dynamic dispatch
- compiler can optimize aggressively
- possible inlining
- usually no vtable lookup
17. Dynamic Dispatch
Rust supports runtime polymorphism through trait objects.
1
2
3
fn speak(animal: &dyn Animal) {
animal.speak();
}
Now:
1
2
3
4
5
let dog = Dog;
let cat = Cat;
speak(&dog);
speak(&cat);
The function receives:
1
&dyn Animal
It does not know the concrete type directly.
Conceptually:
1
2
3
4
5
&dyn Animal
│
├── Dog → Dog::speak()
│
└── Cat → Cat::speak()
The correct method is selected at runtime.
18. Trait Objects
The most common forms are:
1
2
3
4
&dyn Trait
Box<dyn Trait>
Arc<dyn Trait>
Rc<dyn Trait>
For example:
1
let animal: Box<dyn Animal> = Box::new(Dog);
This allows the concrete type to be hidden behind the trait interface.
19. Different Types in One Collection
This is one of the main benefits of trait objects.
You cannot do:
1
let animals = vec![Dog, Cat];
because a vector normally contains one concrete type.
Instead:
1
2
3
4
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
Now:
1
2
3
4
Vec<Box<dyn Animal>>
│
├── Dog
└── Cat
We can iterate:
1
2
3
for animal in animals {
animal.speak();
}
This is dynamic polymorphism.
20. Trait Object vs Enum
You previously saw an enum solution:
1
2
3
4
5
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
An enum explicitly lists all possible variants.
1
2
3
4
5
SpreadsheetCell
│
├── Int
├── Float
└── Text
A trait object does not require listing every possible implementation:
1
Box<dyn Animal>
Any type implementing Animal can potentially be stored there.
1
2
3
4
5
6
dyn Animal
│
├── Dog
├── Cat
├── Horse
└── ...
A useful rule:
Use an enum when the set of possibilities is known and part of your data model.
Use a trait when you want different types to provide common behavior.
21. Associated Types
Traits can define associated types.
1
2
3
4
5
trait Container {
type Item;
fn get(&self) -> Self::Item;
}
Implementation:
1
2
3
4
5
6
7
8
9
struct NumberBox;
impl Container for NumberBox {
type Item = i32;
fn get(&self) -> Self::Item {
42
}
}
Another implementation could use another type:
1
2
3
4
5
6
7
8
9
struct TextBox;
impl Container for TextBox {
type Item = String;
fn get(&self) -> Self::Item {
String::from("hello")
}
}
The associated type is chosen by the implementation.
22. Associated Type vs Generic Trait
Compare:
1
2
3
trait Container {
type Item;
}
with:
1
2
trait Container<T> {
}
An associated type generally means:
For a particular implementation of this trait, there is one associated
Itemtype.
For example:
1
2
3
impl Container for NumberBox {
type Item = i32;
}
A generic trait can allow multiple implementations with different type parameters:
1
2
trait Container<T> {
}
This distinction is important in APIs such as Iterator:
1
2
3
4
5
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
23. Associated Constants
Traits can also define constants.
1
2
3
4
5
trait Config {
const MAX_SIZE: usize;
fn validate(&self) -> bool;
}
Implementation:
1
2
3
4
5
6
7
8
9
struct NetworkConfig;
impl Config for NetworkConfig {
const MAX_SIZE: usize = 1500;
fn validate(&self) -> bool {
true
}
}
Access:
1
println!("{}", NetworkConfig::MAX_SIZE);
24. Supertraits
A trait can require another trait.
1
2
3
trait Animal: Display {
fn speak(&self);
}
This means:
Every
Animalmust also implementDisplay.
Then:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Dog;
impl Display for Dog {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Dog")
}
}
impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}
Conceptually:
1
2
3
4
5
6
7
Display
↑
│ required
Animal
↑
│
Dog
This is sometimes called a trait inheritance relationship, but it is not class inheritance.
25. Multiple Supertraits
You can require multiple traits:
1
2
3
trait Animal: Display + Debug + Send {
fn speak(&self);
}
An implementation must satisfy all of them.
1
2
3
4
Animal
├── Display
├── Debug
└── Send
26. Blanket Implementations
Rust allows implementing a trait for every type satisfying some condition.
Example:
1
2
3
trait Summary {
fn summary(&self) -> String;
}
Suppose every type implementing Display should automatically implement Summary:
1
2
3
4
5
impl<T: Display> Summary for T {
fn summary(&self) -> String {
format!("{self}")
}
}
Now any Display type automatically has Summary.
This is called a blanket implementation.
Conceptually:
1
2
3
4
5
6
Any T
│
└── if T: Display
│
▼
Summary
Blanket implementations are heavily used in Rust libraries.
27. Trait Implementations for References
A trait can be implemented for references too.
1
2
3
4
5
6
7
8
9
trait Speak {
fn speak(&self);
}
impl Speak for Dog {
fn speak(&self) {
println!("Woof");
}
}
You can also implement a trait for:
1
2
3
4
5
impl Speak for &Dog {
fn speak(&self) {
println!("Woof");
}
}
This is useful when behavior differs depending on the exact type being implemented.
However, many standard traits are designed so that implementations for references can often be achieved through borrowing and method resolution without explicitly writing these implementations.
28. Trait Implementations for Generic Types
You can implement a trait for a generic struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Container<T> {
value: T,
}
trait Describe {
fn describe(&self);
}
impl<T: Display> Describe for Container<T> {
fn describe(&self) {
println!("{}", self.value);
}
}
Now:
1
2
3
4
5
let container = Container {
value: 42,
};
container.describe();
The implementation only exists when T: Display.
29. Conditional Trait Implementations
You can conditionally implement a trait:
1
2
3
impl<T: Clone> MyTrait for Container<T> {
// ...
}
This means:
Container<T>implementsMyTraitonly whenTimplementsClone.
This is a very powerful generic programming technique.
30. Trait Bounds on impl Blocks
You can put the bound directly on the implementation:
1
2
3
4
5
6
7
8
impl<T> Container<T>
where
T: Display,
{
fn print(&self) {
println!("{}", self.value);
}
}
This method is available only when T: Display.
31. Trait Bounds on Methods
You can also restrict individual methods:
1
2
3
4
5
6
7
8
9
10
11
12
impl<T> Container<T> {
fn value(&self) -> &T {
&self.value
}
fn print(&self)
where
T: Display,
{
println!("{}", self.value);
}
}
Now value() works for every T, while print() only works when T: Display.
32. Generic Traits
Traits themselves can have generic parameters:
1
2
3
trait Converter<T> {
fn convert(&self, value: T) -> String;
}
Implementation:
1
2
3
4
5
6
7
struct Formatter;
impl Converter<i32> for Formatter {
fn convert(&self, value: i32) -> String {
value.to_string()
}
}
The trait is parameterized by T.
33. Associated Types Are Often Preferred
Instead of:
1
2
3
trait Converter<T> {
fn convert(&self, value: T);
}
you may use:
1
2
3
4
5
trait Converter {
type Input;
fn convert(&self, value: Self::Input);
}
Then:
1
2
3
4
5
6
7
8
9
struct Formatter;
impl Converter for Formatter {
type Input = i32;
fn convert(&self, value: i32) {
println!("{value}");
}
}
Which design is better depends on whether multiple implementations with different parameters should be possible.
34. Generic Methods Inside Traits
A trait method can itself be generic:
1
2
3
trait Printer {
fn print<T: Display>(&self, value: T);
}
Implementation:
1
2
3
4
5
6
7
struct ConsolePrinter;
impl Printer for ConsolePrinter {
fn print<T: Display>(&self, value: T) {
println!("{value}");
}
}
Now:
1
2
3
4
5
let printer = ConsolePrinter;
printer.print(42);
printer.print("hello");
printer.print(3.14);
35. Lifetimes in Traits
Traits can also use lifetimes.
For example:
1
2
3
trait Parser<'a> {
fn parse(&self, input: &'a str) -> &'a str;
}
The lifetime parameter connects the input and output references.
Traits can also use lifetime bounds:
1
2
3
trait Trait<'a> {
fn process(&self, value: &'a str);
}
36. Trait Bounds with Lifetimes
You can combine lifetimes and traits:
1
2
3
4
5
6
fn process<'a, T>(value: &'a T)
where
T: Display,
{
println!("{value}");
}
Or:
1
2
3
fn process<'a, T: Display>(value: &'a T) {
println!("{value}");
}
37. Self in Traits
Inside a trait, Self means:
The concrete type implementing this trait.
Example:
1
2
3
trait Animal {
fn create() -> Self;
}
For:
1
2
3
4
5
impl Animal for Dog {
fn create() -> Self {
Dog
}
}
Self means Dog.
For:
1
2
3
4
5
impl Animal for Cat {
fn create() -> Self {
Cat
}
}
Self means Cat.
38. Returning Self
A common trait pattern is:
1
2
3
trait Builder {
fn new() -> Self;
}
This allows each implementation to construct itself.
1
2
3
4
5
6
7
struct Server;
impl Builder for Server {
fn new() -> Self {
Server
}
}
39. Traits Returning Self
You can also write:
1
2
3
trait Cloneable {
fn duplicate(&self) -> Self;
}
Each implementation returns its own concrete type.
1
2
3
4
5
impl Cloneable for Dog {
fn duplicate(&self) -> Self {
Dog
}
}
40. Operators Use Traits
Many Rust operators are implemented through traits.
For example:
1
a + b
is associated with the Add trait.
Conceptually:
1
2
3
4
5
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
So operator syntax is often just convenient syntax over trait behavior.
Examples include:
1
2
3
4
5
6
7
+ Add
- Sub
* Mul
/ Div
== PartialEq
< PartialOrd
[] Index
This is an important Rust idea:
Much of Rust’s language behavior is built around traits.
41. Common Standard Traits
You will frequently encounter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Debug
Display
Clone
Copy
PartialEq
Eq
PartialOrd
Ord
Hash
Default
Iterator
IntoIterator
From
Into
AsRef
AsMut
Borrow
Deref
DerefMut
Send
Sync
Examples:
1
2
3
4
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
}
The derive attribute automatically generates implementations for supported traits.
42. derive
Instead of manually implementing:
1
2
3
impl Debug for User {
// ...
}
you can often write:
1
2
3
4
#[derive(Debug)]
struct User {
name: String,
}
Rust generates the implementation.
Multiple traits:
1
2
3
4
#[derive(Debug, Clone, PartialEq, Eq)]
struct User {
name: String,
}
43. Fully Qualified Syntax
Sometimes multiple traits provide methods with the same name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("Pilot flying");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Wizard flying");
}
}
Now:
1
let human = Human;
This is ambiguous:
1
human.fly();
We can explicitly select the trait:
1
2
Pilot::fly(&human);
Wizard::fly(&human);
Or use fully qualified syntax:
1
2
<Human as Pilot>::fly(&human);
<Human as Wizard>::fly(&human);
This syntax is especially useful when associated functions are ambiguous.
44. Fully Qualified Syntax for Associated Functions
Suppose:
1
2
3
4
5
6
7
8
9
10
11
trait Animal {
fn create() -> Self;
}
struct Dog;
impl Animal for Dog {
fn create() -> Self {
Dog
}
}
You can write:
1
let dog = Dog::create();
But if multiple traits define create, you may need:
1
let dog = <Dog as Animal>::create();
The general syntax is:
1
<Type as Trait>::function(...)
45. Calling Trait Methods with UFCS
UFCS means Universal Function Call Syntax.
For example:
1
<Dog as Animal>::speak(&dog);
This explicitly says:
Call the
Animalimplementation ofspeakforDog.
It is useful when Rust cannot determine which implementation you mean.
46. Trait Objects and Object Safety
Not every trait can be used as:
1
dyn Trait
For example, a trait containing:
1
2
3
trait Example {
fn create() -> Self;
}
cannot generally be used as a trait object because Self must represent the concrete type.
Similarly, methods that are generic often prevent a trait from being used as a trait object.
For example:
1
2
3
trait Example {
fn process<T>(&self, value: T);
}
This cannot be used normally as:
1
dyn Example
because the runtime vtable cannot contain an implementation for every possible T.
A useful simplified rule is:
Trait objects require methods whose behavior can be represented through a runtime vtable.
Modern Rust documentation often refers to these requirements using the concept of dyn compatibility.
47. Trait Objects Usually Need Indirection
This does not work:
1
let animal: dyn Animal = Dog;
because dyn Animal has an unknown size at compile time.
Instead, use a pointer-like type:
1
let animal: Box<dyn Animal> = Box::new(Dog);
or:
1
let animal: &dyn Animal = &dog;
or:
1
let animal: Arc<dyn Animal> = Arc::new(Dog);
The pointer has a known size even though the concrete object does not.
48. dyn Trait and Dynamic Dispatch
When you write:
1
let animal: &dyn Animal = &dog;
the type is a trait object.
Conceptually:
1
2
3
4
5
6
&dyn Animal
┌───────────────────┐
│ pointer to Dog │
├───────────────────┤
│ pointer to vtable │
└───────────────────┘
The vtable contains information needed for dynamic dispatch.
Therefore:
1
animal.speak();
can find the correct implementation at runtime.
49. Static vs Dynamic Dispatch
The most important comparison:
1
2
3
fn speak<T: Animal>(animal: &T) {
animal.speak();
}
uses static dispatch.
1
2
3
fn speak(animal: &dyn Animal) {
animal.speak();
}
uses dynamic dispatch.
Think:
1
2
3
4
5
6
7
8
9
T: Animal
│
└── compiler knows concrete type
└── static dispatch
dyn Animal
│
└── concrete type hidden
└── runtime dispatch
Static dispatch generally gives the compiler more opportunities for optimization.
Dynamic dispatch gives more flexibility at runtime.
50. Choosing Static or Dynamic Dispatch
Prefer static dispatch when:
1
fn process<T: Animal>(animal: &T)
is sufficient.
Benefits:
- compile-time resolution
- usually no vtable lookup
- easier optimization
- often faster
Use dynamic dispatch when you need runtime polymorphism:
1
fn process(animal: &dyn Animal)
For example, when you need:
1
Vec<Box<dyn Animal>>
containing different concrete types.
A useful rule:
Generics are usually about compile-time polymorphism.
dyn Traitis about runtime polymorphism.
51. Traits and Encapsulation
Traits also help with encapsulation.
Suppose:
1
2
3
pub struct Database {
connection: String,
}
The field is private.
You can expose behavior instead:
1
2
3
4
pub trait Repository {
fn save(&self);
fn load(&self);
}
Then expose only the trait interface.
The caller can depend on:
1
&dyn Repository
without knowing the concrete implementation.
1
2
3
4
5
6
7
8
Application
│
▼
Repository trait
│
├── PostgreSQLRepository
├── MemoryRepository
└── MockRepository
This is particularly useful for designing clean boundaries.
52. Traits and Testing
Traits are useful for dependency injection and testing.
Suppose production code uses:
1
2
3
trait Storage {
fn get(&self, key: &str) -> String;
}
Production implementation:
1
2
3
4
5
6
7
8
struct DatabaseStorage;
impl Storage for DatabaseStorage {
fn get(&self, key: &str) -> String {
// database access
String::from("real value")
}
}
Test implementation:
1
2
3
4
5
6
7
struct MockStorage;
impl Storage for MockStorage {
fn get(&self, key: &str) -> String {
String::from("fake value")
}
}
The application can work with the trait instead of depending directly on the database.
53. Traits and Dependency Injection
For example:
1
2
3
4
fn process<S: Storage>(storage: &S) {
let value = storage.get("key");
println!("{value}");
}
Production:
1
process(&DatabaseStorage);
Test:
1
process(&MockStorage);
This is one reason traits are heavily used in Rust systems programming.
54. Traits and Ownership
Traits do not change Rust’s ownership rules.
For example:
1
2
3
trait Animal {
fn speak(&self);
}
If you have:
1
let dog = Dog;
you still need to respect ownership when passing it around.
You can borrow:
1
speak(&dog);
or move:
1
2
3
fn consume<T: Animal>(animal: T) {
// owns animal
}
Traits describe behavior; ownership determines how the value is accessed and moved.
55. Traits and Send / Sync
Send and Sync are also traits.
For example:
1
2
3
fn spawn<T: Send + 'static>(value: T) {
// ...
}
This means T must satisfy the requirements for being transferred to another thread.
Similarly:
1
2
fn share<T: Sync>(value: &T) {
}
requires that references to T can safely be shared between threads.
This demonstrates an important property of Rust:
Traits are not only for application-level interfaces. They are also used by the language and standard library to express important type properties.
56. Traits Can Express Capabilities
A useful way to think about traits is:
1
Trait = capability / behavior / contract
For example:
1
Clone
means:
This type knows how to make a duplicate.
1
Iterator
means:
This type can produce a sequence of values.
1
Send
means:
This type can be transferred between threads.
1
Display
means:
This type knows how to be formatted for users.
1
Debug
means:
This type provides a debugging representation.
57. Trait Bounds as Requirements
When you write:
1
2
fn process<T: Clone + Debug>(value: T) {
}
you are effectively saying:
1
2
3
4
5
6
7
I don't care exactly what T is.
I only require that:
T implements Clone
AND
T implements Debug
This is one of the most powerful ideas in Rust.
The function depends on behavior, not a concrete type.
58. Traits vs Concrete Types
Badly coupled code might look like:
1
2
3
fn process(database: &PostgresDatabase) {
// ...
}
The function is tied to PostgreSQL.
Trait-based code:
1
2
3
fn process<S: Storage>(storage: &S) {
// ...
}
Now the function only cares that S implements Storage.
This allows:
1
2
3
4
PostgresStorage
MemoryStorage
MockStorage
FileStorage
to all work with the same function.
59. The Most Important Trait Syntax
Here is a compact syntax reference.
Define a trait
1
2
3
trait TraitName {
fn method(&self);
}
Implement a trait
1
2
3
4
impl TraitName for MyType {
fn method(&self) {
}
}
Default method
1
2
3
4
trait TraitName {
fn method(&self) {
}
}
Associated type
1
2
3
trait TraitName {
type Item;
}
Associated constant
1
2
3
trait TraitName {
const VALUE: usize;
}
Associated function
1
2
3
trait TraitName {
fn create() -> Self;
}
Generic trait
1
2
trait TraitName<T> {
}
Trait bound
1
2
fn process<T: TraitName>(value: T) {
}
Multiple bounds
1
2
fn process<T: TraitA + TraitB>(value: T) {
}
where
1
2
3
4
5
fn process<T>(value: T)
where
T: TraitA + TraitB,
{
}
impl Trait parameter
1
2
fn process(value: impl TraitName) {
}
impl Trait return type
1
2
3
fn create() -> impl TraitName {
MyType
}
Trait object
1
2
fn process(value: &dyn TraitName) {
}
Heap-allocated trait object
1
let value: Box<dyn TraitName> = Box::new(MyType);
Reference-counted trait object
1
let value: Rc<dyn TraitName> = Rc::new(MyType);
Thread-safe shared trait object
1
let value: Arc<dyn TraitName + Send + Sync> = Arc::new(MyType);
Supertrait
1
2
trait Child: Parent {
}
Multiple supertraits
1
2
trait Child: ParentA + ParentB {
}
Blanket implementation
1
2
impl<T: SomeTrait> AnotherTrait for T {
}
Conditional implementation
1
2
3
4
5
impl<T> SomeTrait for Container<T>
where
T: Clone,
{
}
Fully qualified syntax
1
<Type as Trait>::method(...)
Generic method
1
2
3
trait Printer {
fn print<T: Display>(&self, value: T);
}
Trait with lifetime
1
2
3
trait Parser<'a> {
fn parse(&self, input: &'a str);
}
60. A Complete Example
Putting several concepts together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use std::fmt::Display;
trait Animal: Display {
const SPECIES: &'static str;
fn speak(&self);
fn description(&self) -> String {
format!("{self}")
}
}
struct Dog {
name: String,
}
struct Cat {
name: String,
}
impl Display for Dog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Dog named {}", self.name)
}
}
impl Animal for Dog {
const SPECIES: &'static str = "Canis familiaris";
fn speak(&self) {
println!("Woof!");
}
}
impl Display for Cat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Cat named {}", self.name)
}
}
impl Animal for Cat {
const SPECIES: &'static str = "Felis catus";
fn speak(&self) {
println!("Meow!");
}
}
Static dispatch:
1
2
3
fn static_speak<T: Animal>(animal: &T) {
animal.speak();
}
Dynamic dispatch:
1
2
3
fn dynamic_speak(animal: &dyn Animal) {
animal.speak();
}
Using them:
1
2
3
4
5
6
7
8
9
10
11
12
13
let dog = Dog {
name: String::from("Rex"),
};
let cat = Cat {
name: String::from("Milo"),
};
static_speak(&dog);
static_speak(&cat);
dynamic_speak(&dog);
dynamic_speak(&cat);
Different types in one collection:
1
2
3
4
5
6
7
8
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(dog),
Box::new(cat),
];
for animal in animals {
animal.speak();
}
61. The Mental Model to Remember
Think about Rust’s type system like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
Trait
│
defines behavior
│
┌────────────┴────────────┐
│ │
Dog Cat
│ │
impl impl
│ │
└────────────┬────────────┘
│
Animal behavior
Then polymorphism can happen in two major ways:
1
2
3
4
5
6
7
8
9
Trait
│
├── Generic T: Trait
│ │
│ └── Static dispatch
│
└── dyn Trait
│
└── Dynamic dispatch
And traits can also be used as constraints:
1
2
3
T: Trait
│
└── "T must provide this capability"
Key Takeaways
- A trait defines shared behavior, not shared data.
- A type implements a trait with
impl Trait for Type. - Rust does not have traditional class inheritance.
- Rust commonly uses composition + traits instead.
- Traits provide one of Rust’s main mechanisms for polymorphism.
T: Traitprovides a trait bound.- Generic trait bounds normally use static dispatch.
dyn Traitprovides dynamic dispatch.Box<dyn Trait>allows different concrete types to be stored together.impl Traitcan be used in function parameters and return types.impl Traitas a return type represents one hidden concrete type.dyn Traitallows the concrete type to vary at runtime.- Traits can contain methods, associated functions, associated types, and associated constants.
- Traits can provide default method implementations.
- A trait can require other traits using supertraits.
- Blanket implementations can implement a trait for every type satisfying a condition.
- Traits can be generic.
- Trait methods can be generic.
- Traits can have lifetime parameters.
Selfrepresents the concrete implementing type.- Fully qualified syntax such as
<Type as Trait>::method()resolves ambiguity. - Many Rust operators and standard-library capabilities are implemented through traits.
SendandSyncare traits too.- Traits allow code to depend on capabilities rather than concrete types.
- This makes traits useful for abstraction, polymorphism, dependency injection, testing, and API design.
The most important mental model is:
Structs describe what a type is made of; traits describe what a type can do.
And for polymorphism:
Generics usually mean static dispatch;
dyn Traitmeans dynamic dispatch.