Code of the Day
IntermediateTypes & traits

Traits

Define shared behaviour with traits, implement them for your types, and use derive macros to get common implementations for free.

RustIntermediate12 min read
By the end of this lesson you will be able to:
  • Define a trait and implement it for a type
  • Provide default method implementations in a trait
  • Implement the standard Display and Debug traits
  • Use derive macros to auto-implement common traits
  • Accept any type that implements a trait using impl Trait syntax

A defines a set of methods that a type must implement. It's Rust's mechanism for shared behaviour — similar to an interface in Java or Go, but more powerful. Unlike inheritance-based polymorphism, traits are opt-in: any type can implement any trait, regardless of where either was defined.

Traits are also how Rust's standard library is organised. Display, Debug, Iterator, From, Into — everything you've used from the standard library is a trait.

Defining a trait

trait Greet {
    fn hello(&self) -> String;
}

This declares that anything implementing Greet must have a hello method returning a String. The trait says nothing about how — that's up to each implementor.

Implementing a trait

struct Person {
    name: String,
}

impl Greet for Person {
    fn hello(&self) -> String {
        format!("Hello, I'm {}", self.name)
    }
}

struct Robot {
    id: u32,
}

impl Greet for Robot {
    fn hello(&self) -> String {
        format!("BEEP. UNIT {} ONLINE.", self.id)
    }
}

Both Person and Robot now implement Greet, and each defines the behaviour in its own way.

Default method implementations

A trait can provide a default implementation that implementors can override:

trait Greet {
    fn hello(&self) -> String;

    fn farewell(&self) -> String {
        format!("{} Goodbye!", self.hello())
    }
}

Any type that implements Greet gets farewell for free. It can still override farewell if a different behaviour is needed.

Default implementations can call other methods from the same trait — including ones without defaults. This lets you define richer behaviour as a consequence of a minimal required surface. The standard library's Iterator trait uses this extensively: implement next(), and you get dozens of methods for free.

Display and Debug

Two traits you'll use constantly:

  • Debug — used by {:?} in format strings. Provides a developer-facing representation.
  • Display — used by {}. Provides a user-facing representation.
use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Point {{ x: {}, y: {} }}", self.x, self.y)
    }
}

derive macros

For many common traits, you don't need to write the implementation — the compiler can generate it automatically using :

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

Derivable traits include Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, and Default. The derive macro generates a sensible implementation based on the struct's fields — all fields must also implement the derived trait.

derive(PartialEq) compares all fields. If your type has a field that shouldn't factor into equality (like a cache or ID), you need to implement PartialEq manually. Don't blindly derive — understand what the derived implementation does.

Using traits as parameters: impl Trait

Accept any type that implements a trait using impl Trait syntax:

fn print_greeting(item: &impl Greet) {
    println!("{}", item.hello());
}

fn main() {
    let p = Person { name: String::from("Ada") };
    let r = Robot { id: 42 };
    print_greeting(&p);
    print_greeting(&r);
}

impl Greet in a parameter position means "any type that implements Greet." The compiler generates a specialised version for each concrete type used — this is called monomorphization, and it means zero runtime overhead compared to a direct call. (The next lesson on generics covers the equivalent <T: Greet> syntax.)

Check your understanding

Knowledge check

  1. 1.
    What happens if you implement a trait for a type and do not provide a method that has a default implementation in the trait?
  2. 2.
    Which macro auto-generates a Debug implementation based on field names and values?
  3. 3.
    You can implement any trait for any type, regardless of where either was defined.

Do it yourself

Define a Summary trait with one required method summarise(&self) -> String and one default method preview(&self) -> String that calls summarise and truncates to 50 characters. Implement it for two structs: Article { title, author, content } and Tweet { username, content }. Then write a function that accepts impl Summary and prints the preview.

Where to go next

impl Trait works well for single parameters. When you have multiple parameters or return types that need to be the same concrete type, you need generics — which give you explicit type parameter names and finer-grained control over trait bounds.

Finished reading? Mark it complete to track your progress.

On this page