Skip to content
AgileLogics
6 min read

Traits

Prerequisites: Generics

A trait describes behavior that several types can share: a set of method signatures a type promises to provide. Traits are similar to interfaces in other languages. They are also what trait bounds (T: PartialOrd) refer to.

Syntax

trait Summary {
    fn author(&self) -> String;             // required: every implementor must write it

    fn summarize(&self) -> String {         // default: implementors get this for free
        format!("(Read more from {}...)", self.author())
    }
}

struct Post {
    user: String,
}

impl Summary for Post {                     // implement the trait for a type
    fn author(&self) -> String {
        format!("@{}", self.user)
    }
}

Ways to accept "any type that implements Summary":

fn notify(item: &impl Summary) { /* ... */ }           // impl Trait in argument position

fn notify_generic<T: Summary>(item: &T) { /* ... */ }  // the same thing, as a trait bound

fn compare<T, U>(a: &T, b: &U)                         // a where clause, for longer bounds
where
    T: Summary + Clone,
    U: Summary,
{ /* ... */ }

Example

trait Summary {
    fn author(&self) -> String;

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

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

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

impl Summary for Article {
    fn author(&self) -> String {
        self.writer.clone()
    }

    // Override the default method.
    fn summarize(&self) -> String {
        format!("{}, by {}", self.title, self.author())
    }
}

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

fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

fn longest_summary<A, B>(a: &A, b: &B) -> String
where
    A: Summary,
    B: Summary,
{
    let (sa, sb) = (a.summarize(), b.summarize());
    if sa.len() >= sb.len() { sa } else { sb }
}

// impl Trait in return position: "returns some type that implements Summary".
fn sample_post() -> impl Summary {
    Post { user: String::from("rustlang"), text: String::from("Rust 2024 is out") }
}

fn main() {
    let article = Article {
        title: String::from("Traits explained"),
        writer: String::from("Ada"),
    };
    let post = Post { user: String::from("ferris"), text: String::from("hello!") };

    notify(&article);
    notify(&post);
    println!("{} says: {}", post.author(), post.text);
    println!("Longest: {}", longest_summary(&article, &post));
    println!("{}", sample_post().summarize());
}

Expected output

Breaking news! Traits explained, by Ada
Breaking news! (Read more from @ferris...)
@ferris says: hello!
Longest: (Read more from @ferris...)
(Read more from @rustlang...)

Trait objects

impl Summary and T: Summary are resolved at compile time, so every call uses one concrete type. To keep different types in one collection, use a trait object, dyn Summary, behind a pointer such as Box or &. Method calls then go through a small lookup table at runtime:

let items: Vec<Box<dyn Summary>> = vec![Box::new(article), Box::new(post)];
for item in &items {
    println!("{}", item.summarize());
}

Rules to know

  • You can implement a trait for a type only if the trait or the type is defined in your crate (the "orphan rule"). You can implement your Summary for Vec<i32>, or the standard Display for your Article, but not the standard Display for the standard Vec.
  • A trait's methods are only callable when the trait is in scope. If a method "doesn't exist", check for a missing use some_crate::SomeTrait;.

Common mistakes

  • Leaving out a required method: error[E0046]: not all trait items implemented, missing: `author` .
  • Returning two different types from a function declared -> impl Summary, for example an Article in one branch and a Post in the other: error[E0308]: `if` and `else` have incompatible types. impl Trait still means one concrete type; return Box<dyn Summary> if it really varies.
  • Writing Vec<dyn Summary> without a pointer. Trait objects have no fixed size: error[E0277]: the size for values of type `dyn Summary` cannot be known at compilation time.
  • Calling a trait method without importing the trait: error[E0599]: no method named `summarize` found for struct `Article` in the current scope, with a hint to add a use line.

Practice

Define a trait Area with a required method area(&self) -> f64 and a default method describe(&self) -> String returning "area = <area>" with two decimal places. Implement it for Circle { radius: f64 } and Rect { w: f64, h: f64 }, then write fn total_area(shapes: &[Box<dyn Area>]) -> f64 and print the description of a circle with radius 1 and the total area of that circle plus a 2 × 3 rectangle.

Show solution
trait Area {
    fn area(&self) -> f64;

    fn describe(&self) -> String {
        format!("area = {:.2}", self.area())
    }
}

struct Circle {
    radius: f64,
}

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

impl Area for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Area for Rect {
    fn area(&self) -> f64 {
        self.w * self.h
    }
}

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

fn main() {
    let circle = Circle { radius: 1.0 };
    println!("{}", circle.describe());

    let shapes: Vec<Box<dyn Area>> = vec![Box::new(circle), Box::new(Rect { w: 2.0, h: 3.0 })];
    println!("total = {:.2}", total_area(&shapes));
}
area = 3.14
total = 9.14