Free Handbook · Every example compiled & verified

Traits & Generics

Define and implement traits, default methods, generic functions and structs, trait bounds, impl Trait, dyn trait objects, the orphan rule and operator overloading.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 07 · what you'll be able to do

  • Define a trait, implement it for several types, and give it default methods
  • Write generic functions, structs and enums, and constrain them with bounds, where clauses and impl Trait
  • Implement Display by hand and derive PartialOrd, knowing what the derived order means
  • Choose between static dispatch (generics) and dynamic dispatch (dyn Trait), and explain the cost of each
  • Read the orphan-rule error, work around it with a newtype, and overload + with std::ops::Add
01

Defining and implementing a trait

A trait is a named set of methods that a type promises to provide, like an interface in Java or TypeScript. You define it once, then write an impl Trait for Type block for each type that should have it. Any type can implement any number of traits, including types that were written long before the trait existed.

rustmain.rs
trait Summary {
    fn author(&self) -> String;                        // required: no body

    fn summarize(&self) -> String {                    // default method
        format!("(Read more from {}...)", self.author())
    }
}

struct Tweet {
    user: String,
    text: String,
}

struct Article {
    title: String,
    by: String,
}

impl Summary for Tweet {
    fn author(&self) -> String {
        format!("@{}", self.user)
    }
    fn summarize(&self) -> String {                    // overrides the default
        format!("{}: {}", self.author(), self.text)
    }
}

impl Summary for Article {
    fn author(&self) -> String {
        self.by.clone()
    }                                                  // keeps the default summarize
}

fn main() {
    let t = Tweet { user: String::from("rustlang"), text: String::from("1.0 is out") };
    let a = Article { title: String::from("Traits"), by: String::from("Ada") };
    println!("{}", t.summarize());
    println!("{} {}", a.title, a.summarize());
}
Outputcompiled & run with real Rust
@rustlang: 1.0 is out
Traits (Read more from Ada...)
Your turn

Add a third type, Podcast { host: String }, and implement only author. Which summarize does it get?

A default method has a body in the trait, and can call the trait's required methods. Implementers get it for free and may override it. This is how the standard library gives you dozens of iterator methods once you write next (Module 09).

Error you will hit

E0046: a required method is missing

rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Square(f64);

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
}

fn main() {
    println!("{}", Square(2.0).area());
}
error[E0046]: not all trait items implemented, missing: `name`
 --> main.rs:8:1
  |
3 |     fn name(&self) -> String;
  |     ------------------------- `name` from trait
...
8 | impl Shape for Square {
  | ^^^^^^^^^^^^^^^^^^^^^ missing `name` in implementation
Why the compiler said that

An impl Shape for Square is a promise to provide every method the trait declares without a body. name has no default, so the promise is broken.

The fix

Implement name, or give it a default body in the trait if most types would write the same thing.

rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Square(f64);

impl Shape for Square {
    fn area(&self) -> f64 {
        self.0 * self.0
    }
    fn name(&self) -> String {
        String::from("square")
    }
}

fn main() {
    let s = Square(2.0);
    println!("{} {}", s.name(), s.area());
}
02

Generic functions and trait bounds

A generic function works for many types. You name a type parameter in angle brackets, fn largest<T>(...), and use T in the signature. On its own T could be anything, so the body may not do anything with it. A trait bound such as T: PartialOrd says "only types that implement this trait", and in return lets the body use that trait's methods and operators.

Error you will hit

E0369: using > on a type with no bound

rust
fn largest<T>(items: &[T]) -> &T {
    let mut best = &items[0];
    for item in items {
        if item > best {
            best = item;
        }
    }
    best
}

fn main() {
    println!("{}", largest(&[3, 9, 4]));
}
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> main.rs:4:17
  |
4 |         if item > best {
  |            ---- ^ ---- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(items: &[T]) -> &T {
  |             ++++++++++++++++++++++
Why the compiler said that

The compiler checks a generic function once, for every possible T, not for each call. Some types cannot be compared with >, so without a bound the body is not valid for all T. The help line names the exact bound to add.

The fix

Add the bound: fn largest<T: PartialOrd>(items: &[T]) -> &T.

rust
fn largest<T: PartialOrd>(items: &[T]) -> &T {
    let mut best = &items[0];
    for item in items {
        if item > best {
            best = item;
        }
    }
    best
}

fn main() {
    println!("{}", largest(&[3, 9, 4]));
}

Bounds can be written three ways. They mean the same thing; pick the one that reads best:

rustmain.rs
use std::fmt::Display;

// 1. Inline bound
fn largest<T: PartialOrd>(items: &[T]) -> &T {
    let mut best = &items[0];
    for item in items {
        if item > best {
            best = item;
        }
    }
    best
}

// 2. where clause: easier to read with several bounds
fn show_pair<A, B>(a: A, b: B) -> String
where
    A: Display,
    B: Display + Clone,           // + means "both traits"
{
    format!("<{a}, {b}>")
}

// 3. impl Trait in argument position: shorthand for a simple bound
fn shout(msg: impl Display) -> String {
    format!("{}!", msg.to_string().to_uppercase())
}

fn main() {
    println!("{}", largest(&[3, 9, 4]));
    println!("{}", largest(&[1.5, -2.0]));
    println!("{}", largest(&["pear", "apple", "zucchini"]));
    println!("{}", show_pair(1, "one"));
    println!("{} {}", shout("hi"), shout(42));
}
Outputcompiled & run with real Rust
9
1.5
zucchini
<1, one>
HI! 42!

The same largest works for integers, floats and string slices because all three implement PartialOrd.

impl Trait in return position

-> impl Trait means "I return one specific type that implements this trait, but I am not telling you which." It is how you return closures and iterator chains, whose real types are unnamed or very long.

rustmain.rs
fn evens_squared(limit: u32) -> impl Iterator<Item = u32> {
    (1..=limit).filter(|n| n % 2 == 0).map(|n| n * n)
}

fn make_adder(k: i32) -> impl Fn(i32) -> i32 {
    move |x| x + k
}

fn main() {
    let v: Vec<u32> = evens_squared(10).collect();
    println!("{:?}", v);
    let add5 = make_adder(5);
    println!("{}", add5(10));
}
Outputcompiled & run with real Rust
[4, 16, 36, 64, 100]
15
One concrete type only
-> impl Trait must return the same type on every path. A function that returns a Circle in one branch and a Square in another cannot use it, even if both implement Shape. That case needs a trait object, Box<dyn Shape> (later in this module).
03

Generic structs and enums

Types can be generic too. You have used them since Module 05: Vec<T>, Option<T>, Result<T, E> and HashMap<K, V> are all ordinary generic types from the standard library. Methods go in impl<T> Type<T>, and you can add methods that exist only for some T.

rustmain.rs
#[derive(Debug)]
struct Pair<T> {
    a: T,
    b: T,
}

impl<T> Pair<T> {                         // for every T
    fn new(a: T, b: T) -> Self {
        Pair { a, b }
    }
    fn swap(self) -> Pair<T> {
        Pair { a: self.b, b: self.a }
    }
}

impl<T: PartialOrd + Copy> Pair<T> {      // only when T can be compared and copied
    fn max(&self) -> T {
        if self.a > self.b { self.a } else { self.b }
    }
}

#[derive(Debug)]
enum Tree<T> {                            // a generic enum
    Leaf(T),
    Node(Box<Tree<T>>, Box<Tree<T>>),
}

fn count<T>(t: &Tree<T>) -> usize {
    match t {
        Tree::Leaf(_) => 1,
        Tree::Node(l, r) => count(l) + count(r),
    }
}

fn main() {
    let p = Pair::new(3, 8);
    println!("{:?} max={}", p, p.max());
    println!("{:?}", p.swap());

    let words = Pair::new(String::from("x"), String::from("y"));
    println!("{:?}", words.swap());         // no max(): String is not Copy

    let t = Tree::Node(Box::new(Tree::Leaf('a')), Box::new(Tree::Node(Box::new(Tree::Leaf('b')), Box::new(Tree::Leaf('c')))));
    println!("leaves: {}", count(&t));
}
Outputcompiled & run with real Rust
Pair { a: 3, b: 8 } max=8
Pair { a: 8, b: 3 }
Pair { a: "y", b: "x" }
leaves: 3

Box puts the child on the heap so the recursive enum has a known size; Module 10 explains why.

Monomorphization: generics cost nothing at run time
For every concrete type you use, the compiler stamps out a separate copy of the generic code: largest::<i32>, largest::<f64>, and so on. Each copy is as fast as if you had written it by hand. The price is paid at compile time and in binary size, not in speed.
04

derive vs a manual impl: Display and PartialOrd

Some traits can be derived (Module 04); others you must write. Display, the trait behind {}, is never derivable, because only you know how your type should look to a user. PartialOrd can be derived, and it is important to know what order the derived version uses: fields top to bottom, compared one after another (for enums: variants in declaration order).

rustmain.rs
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Priority {
    Low,                  // declared first = smallest
    Medium,
    High,
}

#[derive(Debug, PartialEq, PartialOrd)]
struct Version {
    major: u32,           // compared first
    minor: u32,           // only if major is equal
    patch: u32,
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
    }
}

fn main() {
    let a = Version { major: 1, minor: 10, patch: 0 };
    let b = Version { major: 1, minor: 9, patch: 7 };
    println!("{a} > {b}? {}", a > b);      // 10 > 9 decides it
    println!("{:?}", b);                   // Debug: derived
    println!("[{:>10}]", a.to_string());   // Display gives you to_string()

    let mut tasks = vec![Priority::Medium, Priority::High, Priority::Low];
    tasks.sort();                          // needs Ord
    println!("{:?} max={:?}", tasks, tasks.iter().max().unwrap());
}
Outputcompiled & run with real Rust
v1.10.0 > v1.9.7? true
Version { major: 1, minor: 9, patch: 7 }
[   v1.10.0]
[Low, Medium, High] max=High

Comparing versions as strings would put "1.9.7" after "1.10.0". Numeric fields in the right order give the right answer for free.

When the derived order is wrong, write the impl yourself. Here a Player is ranked by score, highest first, and the name must not take part:

rustmain.rs
use std::cmp::Ordering;

#[derive(Debug, PartialEq)]
struct Player {
    name: String,
    score: u32,
}

impl PartialOrd for Player {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        other.score.partial_cmp(&self.score)       // reversed: higher score sorts first
    }
}

fn main() {
    let mut board = vec![
        Player { name: String::from("kim"), score: 40 },
        Player { name: String::from("ana"), score: 95 },
        Player { name: String::from("raj"), score: 70 },
    ];
    board.sort_by(|x, y| x.partial_cmp(y).unwrap());
    for p in &board {
        println!("{:<4}{}", p.name, p.score);
    }
}
Outputcompiled & run with real Rust
ana 95
raj 70
kim 40
Your turn

In real code you would usually skip the impl and write board.sort_by_key(|p| std::cmp::Reverse(p.score)). Try it. When is a trait impl the better choice? (Hint: when the type is compared in many places.)

Keep PartialEq and PartialOrd consistent
Here two players with the same score but different names are not equal (PartialEq is derived over both fields) yet compare as Equal. That is allowed for sorting but breaks the contract other code relies on. If you hand-write the ordering, hand-write equality over the same fields too.
05

Trait objects: dyn Trait and dynamic dispatch

Generics require one concrete type per use: a Vec<T> holds only circles, or only squares. To hold different types that share a trait in one collection, use a trait object: Box<dyn Shape> or &dyn Shape. The concrete type is forgotten; what is kept is a pointer to the data plus a pointer to a vtable, a table of that type's method addresses. Each call looks the method up in the table at run time.

rustmain.rs
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { 3.14159 * self.r * self.r }
    fn name(&self) -> &str { "circle" }
}

impl Shape for Rect {
    fn area(&self) -> f64 { self.w * self.h }
    fn name(&self) -> &str { "rect" }
}

fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { r: 1.0 }),
        Box::new(Rect { w: 2.0, h: 3.0 }),
        Box::new(Circle { r: 0.5 }),
    ];
    for s in &shapes {
        println!("{:<6} {:.2}", s.name(), s.area());
    }
    println!("total  {:.2}", total_area(&shapes));
}
Outputcompiled & run with real Rust
circle 3.14
rect   6.00
circle 0.79
total  9.93
VisualizeOne call, two different methodsStep 1 / 4
for s in &shapes {
println!("{}", s.area());
}
// shapes = [Box<Circle>, Box<Rect>]
Line 1

First element. s is a &Box<dyn Shape>: a pointer to the Circle plus a pointer to Circle's vtable.

Variables now
s(ptr to Circle { r: 1.0 }, Circle vtable)
All 4 steps as a table
StepLineWhat happenedVariables now
11First element. s is a &Box<dyn Shape>: a pointer to the Circle plus a pointer to Circle's vtable.s = (ptr to Circle { r: 1.0 }, Circle vtable)
22s.area() reads the area slot of the vtable, finds Circle::area, and calls it with the data pointer as self.
31Second element: same static type, different vtable.s = (ptr to Rect { w: 2.0, h: 3.0 }, Rect vtable)
42The same line of code now lands in Rect::area. That run-time choice is dynamic dispatch.
Static dispatch (generics, impl Trait)Dynamic dispatch (dyn Trait)
Written asfn f<T: Shape>(s: &T) / impl Shapefn f(s: &dyn Shape) / Box<dyn Shape>
Method chosenAt compile time; can be inlinedAt run time through the vtable
Mixed types in one VecNoYes
Binary sizeOne copy per concrete typeOne copy
SpeedFastestOne indirect call per method; usually negligible
RestrictionNoneTrait must be dyn compatible: no generic methods, no Self returned by value
Error you will hit

E0782: a trait used as a type without dyn

rust
trait Shape {
    fn area(&self) -> f64;
}

struct Sq(f64);
impl Shape for Sq {
    fn area(&self) -> f64 { self.0 * self.0 }
}

fn main() {
    let s: Box<Shape> = Box::new(Sq(2.0));
    println!("{}", s.area());
}
error[E0782]: expected a type, found a trait
  --> main.rs:11:16
   |
11 |     let s: Box<Shape> = Box::new(Sq(2.0));
   |                ^^^^^
   |
help: you can add the `dyn` keyword if you want a trait object
   |
11 |     let s: Box<dyn Shape> = Box::new(Sq(2.0));
   |                +++
Why the compiler said that

In the 2021 edition a bare trait name is not a type. Rust wants you to say which kind of "some Shape" you mean: a trait object (dyn Shape, chosen at run time) or a generic/impl Shape (one type, chosen at compile time).

The fix

Write Box<dyn Shape>. The dyn keyword makes the run-time cost visible in the code.

rust
trait Shape {
    fn area(&self) -> f64;
}

struct Sq(f64);
impl Shape for Sq {
    fn area(&self) -> f64 { self.0 * self.0 }
}

fn main() {
    let s: Box<dyn Shape> = Box::new(Sq(2.0));
    println!("{}", s.area());
}
Which one teams reach for
Rust code defaults to generics, and switches to dyn Trait for plugin lists, heterogeneous collections, and to keep compile times and binary size down in large codebases. Both appear in interviews as "static vs dynamic dispatch", so be ready to explain the vtable.
06

The orphan rule and the newtype pattern

You may write impl Trait for Type only if the trait or the type is defined in your crate. Implementing a foreign trait for a foreign type (say, Display for Vec<i32>) is forbidden. This orphan rule guarantees that two libraries can never both provide conflicting impls for the same pair, which would make your program ambiguous the moment you used both.

Error you will hit

E0117: implementing a foreign trait for a foreign type

rust
use std::fmt;

impl fmt::Display for Vec<i32> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} numbers", self.len())
    }
}

fn main() {
    println!("{}", vec![1, 2, 3]);
}
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
 --> main.rs:3:1
  |
3 | impl fmt::Display for Vec<i32> {
  | ^^^^^^^^^^^^^^^^^^^^^^--------
  |                       |
  |                       `Vec` is not defined in the current crate
  |
  = note: impl doesn't have any local type before any uncovered type parameters
  = note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
  = note: define and implement a trait or new type instead
Why the compiler said that

Display belongs to the standard library, and so does Vec. Neither is local to your crate, so the impl is an orphan.

The fix

Wrap the foreign type in a local newtype (a one-field tuple struct, Module 04) and implement the trait for the wrapper. It costs nothing at run time.

rust
use std::fmt;

struct Numbers(Vec<i32>);

impl fmt::Display for Numbers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} numbers", self.0.len())
    }
}

fn main() {
    println!("{}", Numbers(vec![1, 2, 3]));
}
rustmain.rs
use std::fmt;

struct Csv(Vec<String>);                // newtype: local, so we may implement Display

impl fmt::Display for Csv {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0.join(","))
    }
}

trait Describe {                         // a local trait...
    fn describe(&self) -> String;
}

impl Describe for i32 {                  // ...may be implemented for a foreign type
    fn describe(&self) -> String {
        if *self < 0 { format!("{self} (negative)") } else { format!("{self}") }
    }
}

fn main() {
    let row = Csv(vec![String::from("id"), String::from("name"), String::from("email")]);
    println!("{row}");
    println!("{} | {}", 7.describe(), (-3).describe());
}
Outputcompiled & run with real Rust
id,name,email
7 | -3 (negative)
07

Associated types and operator overloading

Some traits have an associated type: a type the implementer picks, named inside the trait. Iterator is the famous one: type Item; says what next returns, and each implementation fixes it once (type Item = u32;). The difference from a generic trait is that a type can implement Iterator only one way, so callers never have to say which Item they mean. Module 09 implements Iterator in full.

Operators are traits too. a + b is std::ops::Add::add(a, b), a == b is PartialEq, a[i] is Index. Implement the trait and your type gets the operator. Add uses an associated type, Output, for the result.

rustmain.rs
use std::ops::{Add, Mul};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
    x: f64,
    y: f64,
}

impl Add for Vec2 {
    type Output = Vec2;                  // associated type: what + returns
    fn add(self, other: Vec2) -> Vec2 {
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
}

impl Mul<f64> for Vec2 {                 // Vec2 * f64 (a different right-hand type)
    type Output = Vec2;
    fn mul(self, k: f64) -> Vec2 {
        Vec2 { x: self.x * k, y: self.y * k }
    }
}

fn sum_all<T: Add<Output = T> + Copy>(items: &[T], zero: T) -> T {
    let mut acc = zero;
    for &i in items {
        acc = acc + i;
    }
    acc
}

fn main() {
    let a = Vec2 { x: 1.0, y: 2.0 };
    let b = Vec2 { x: 0.5, y: -1.0 };
    println!("{:?}", a + b);
    println!("{:?}", a * 3.0);
    println!("{}", a + b == Vec2 { x: 1.5, y: 1.0 });
    println!("{:?}", sum_all(&[a, b, a], Vec2 { x: 0.0, y: 0.0 }));
    println!("{}", sum_all(&[1, 2, 3], 0));   // the same function on i32
}
Outputcompiled & run with real Rust
Vec2 { x: 1.5, y: 1.0 }
Vec2 { x: 3.0, y: 6.0 }
true
Vec2 { x: 2.5, y: 3.0 }
6

T: Add<Output = T> constrains the associated type in a bound: "T can be added, and adding gives another T."

Error you will hit

E0369: + on a type that does not implement Add

rust
#[derive(Debug)]
struct Money {
    cents: i64,
}

fn main() {
    let a = Money { cents: 150 };
    let b = Money { cents: 275 };
    println!("{:?}", a + b);
}
error[E0369]: cannot add `Money` to `Money`
  --> main.rs:9:24
   |
 9 |     println!("{:?}", a + b);
   |                      - ^ - Money
   |                      |
   |                      Money
   |
note: an implementation of `Add` might be missing for `Money`
  --> main.rs:2:1
   |
 2 | struct Money {
   | ^^^^^^^^^^^^ must implement `Add`
Why the compiler said that

There is no impl Add for Money, so + has nothing to call. Rust never adds structs field by field on its own: for Money that would be right, for a Date it would be nonsense.

The fix

Implement std::ops::Add with type Output = Money.

rust
use std::ops::Add;

#[derive(Debug)]
struct Money {
    cents: i64,
}

impl Add for Money {
    type Output = Money;
    fn add(self, other: Money) -> Money {
        Money { cents: self.cents + other.cents }
    }
}

fn main() {
    let a = Money { cents: 150 };
    let b = Money { cents: 275 };
    println!("{:?}", a + b);
}
Trait
A named set of methods a type can implement; Rust's interfaces.
Default method
A trait method with a body, inherited by implementers unless they override it.
Generic
Code with a type parameter, <T>, that works for many types.
Trait bound
T: Trait: restricts a type parameter to types implementing the trait, and unlocks its methods.
where clause
The same bounds written after the signature, for readability.
impl Trait
In arguments: any type with the trait. In returns: one hidden concrete type with the trait.
Monomorphization
Compiling a separate copy of generic code for each concrete type used. Zero run-time cost.
Trait object (dyn Trait)
A pointer to a value plus a vtable, so different types can be used through one trait at run time.
Dynamic dispatch
Choosing which method to call at run time through the vtable.
Orphan rule
You may implement a trait for a type only if the trait or the type is local to your crate (E0117).
Associated type
A type named inside a trait and fixed by each impl, e.g. Iterator::Item, Add::Output.
Quick check

You need a Vec that holds both Circle and Rect values and calls area() on each. Which type do you use?

Quick check

Why does impl std::fmt::Display for Vec fail to compile in your crate?

Frequently asked questions

Are Rust traits the same as interfaces?
Close. Like interfaces, traits declare methods a type must provide. Unlike most interfaces, they can have default method bodies, associated types and constants, can be implemented for types you did not write (within the orphan rule), and are used both for compile-time generics and run-time trait objects.
What is the difference between impl Trait and dyn Trait?
impl Trait is static dispatch: one concrete type, known to the compiler, with calls that can be inlined. dyn Trait is dynamic dispatch: the concrete type is erased, calls go through a vtable at run time, and it lets you mix different types in one collection.
Do Rust generics slow the program down?
No. Generics are monomorphized: the compiler generates a specialised copy for each concrete type, so generic code runs as fast as hand-written code. The costs are longer compile times and larger binaries.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.