Post

Rust Notes: Traits

A review of traits in Rust.

Rust Notes: Traits

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 Animal must provide a speak method.

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:

T must implement Animal.

The syntax:

1
T: Trait

means:

T implements Trait.


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 Item type.

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 Animal must also implement Display.

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> implements MyTrait only when T implements Clone.

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 Animal implementation of speak for Dog.

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 Trait is 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: Trait provides a trait bound.
  • Generic trait bounds normally use static dispatch.
  • dyn Trait provides dynamic dispatch.
  • Box<dyn Trait> allows different concrete types to be stored together.
  • impl Trait can be used in function parameters and return types.
  • impl Trait as a return type represents one hidden concrete type.
  • dyn Trait allows 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.
  • Self represents 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.
  • Send and Sync are 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 Trait means dynamic dispatch.

This post is licensed under CC BY 4.0 by the author.