3 min read
On this page

Defining & Implementing Traits

Traits are Rust's answer to interfaces, type classes, and protocols. They define shared behavior that types can implement, and they are the foundation of polymorphism in Rust. If you have written Go interfaces or Haskell type classes, traits will feel familiar — but with Rust's ownership system adding an extra dimension.

The trait Keyword

A trait declares a set of methods that a type must implement:

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

Any type that implements Summary must provide a summarize method. The compiler enforces this at compile time — there is no way to forget.

Implementing Traits for Your Types

Use impl TraitName for TypeName to wire a trait to a type:

struct Article {
    title: String,
    author: String,
    content: String,
}

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{} by {}", self.title, self.author)
    }
}

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

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.text)
    }
}

Now both Article and Tweet can be passed to any function that expects a Summary.

Default Methods

Traits can provide default implementations. Types can override them or accept the default:

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

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

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

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

Default methods can call other methods in the same trait, even ones without defaults. This is a powerful pattern for building layered APIs where implementors only need to provide the primitives.

Implementing External Traits

You can implement an external trait for your type, or your trait for an external type, but not an external trait for an external type. This is the orphan rule, and it prevents conflicting implementations across crates:

use std::fmt;

struct Color {
    r: u8,
    g: u8,
    b: u8,
}

// Your type, external trait — allowed
impl fmt::Display for Color {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
    }
}

If you need to implement an external trait for an external type, use the newtype pattern: wrap the external type in a tuple struct.

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

The Traits You Use Daily

Display & Debug

Display is for user-facing output ({}). Debug is for developer-facing output ({:?}). Derive Debug; implement Display manually:

#[derive(Debug)]
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)
    }
}

fn main() {
    let p = Point { x: 1.0, y: 2.5 };
    println!("Debug: {:?}", p);   // Debug: Point { x: 1.0, y: 2.5 }
    println!("Display: {}", p);   // Display: (1, 2.5)
}

Clone & Copy

Clone provides explicit duplication via .clone(). Copy enables implicit bitwise copying for simple types. Copy requires Clone:

#[derive(Debug, Clone, Copy)]
struct Velocity {
    dx: f64,
    dy: f64,
}

#[derive(Debug, Clone)]
struct Config {
    name: String,  // String is not Copy, so Config cannot be Copy
    retries: u32,
}

PartialEq & Eq

PartialEq enables == and !=. Eq is a marker trait asserting that equality is reflexive (every value equals itself). Most types derive both; floating-point types only implement PartialEq because NaN != NaN:

#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);

fn main() {
    let a = UserId(42);
    let b = UserId(42);
    assert_eq!(a, b);
}

Trait Objects vs Generics

There are two ways to use traits polymorphically. Generics resolve at compile time (static dispatch). Trait objects resolve at runtime (dynamic dispatch):

// Static dispatch — compiler generates specialized code for each type
fn print_summary(item: &impl Summary) {
    println!("{}", item.summarize());
}

// Dynamic dispatch — uses a vtable at runtime
fn print_summary_dyn(item: &dyn Summary) {
    println!("{}", item.summarize());
}

Generics produce faster code because the compiler inlines the concrete methods. Trait objects produce smaller binaries and allow heterogeneous collections. The choice depends on whether you need uniform types in a container or raw performance in a hot path.

// Heterogeneous collection requires trait objects
fn print_all(items: &[&dyn Summary]) {
    for item in items {
        println!("{}", item.summarize());
    }
}

fn main() {
    let article = Article {
        title: "Rust Traits".into(),
        author: "Jane".into(),
        content: "...".into(),
    };
    let tweet = Tweet {
        username: "rustlang".into(),
        text: "Traits are great".into(),
    };
    print_all(&[&article, &tweet]);
}

Supertraits

A trait can require that implementors also implement another trait:

trait PrettyPrint: fmt::Display {
    fn pretty_print(&self) {
        println!("=== {} ===", self); // can use Display because it's a supertrait
    }
}

Any type implementing PrettyPrint must also implement Display. This is how you build trait hierarchies without inheritance.

Common Pitfalls

  • Forgetting the orphan rule — you cannot implement Display for Vec<T> directly. Use a newtype wrapper.
  • Deriving too much — deriving Clone on a type that holds a database connection or file handle compiles fine but creates semantic bugs.
  • Default method coupling — a default method that calls three other trait methods creates a fragile contract. Keep defaults simple.
  • Trait object limitations — not all traits can be used as dyn Trait. Methods returning Self or using generics break object safety (covered in detail in the dynamic dispatch subtopic).
  • Implementing Display but not Debug — Debug is expected everywhere in Rust. Always derive it, even if you also implement Display.

Key Takeaways

  • Traits define shared behavior. Use impl Trait for Type to implement them.
  • Default methods let you build layered APIs with minimal required implementations.
  • The orphan rule prevents conflicting implementations across crates. The newtype pattern works around it.
  • Debug, Display, Clone, PartialEq — derive what you can, implement what you must.
  • Generics give you static dispatch and speed. Trait objects give you dynamic dispatch and flexibility. Pick based on the problem.