Skip to content
intermediate Phase 6 · Structs, Traits, and Trait Bounds

Traits and Implementations

Define shared method signatures and implement them for any compatible type.

50m
0 problems
Topic Progress 0%

Declaring a Trait and Writing the First `impl Trait for Type`

You have two structs, Article and Video, and each grows its own summary() function with the same signature and a different body. Every new type repeats the pattern, and the function that consumes summaries must know every concrete type in advance. A trait fixes that by naming the behavior once:

pub struct Article { pub title: String, pub body: String }
pub struct Video   { pub title: String, pub seconds: u32 }

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

A trait is a named contract: method signatures with no data and no storage behind them. impl Blocks and Methods attached behavior to a type you own with impl Type { ... }; sharing behavior across many types uses a second form, impl Trait for Type, and that is the only way to attach a trait's methods to a type:

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

impl Summary for Video {
    fn summarize(&self) -> String {
        format!("{} ({}s)", self.title, self.seconds)
    }
}

fn main() {
    let a = Article { title: "Traits".into(), body: "long text".into() };
    println!("{}", a.summarize());   // Traits (9 chars)
}

The compiler enforces three things on every impl. First, the signature must match the trait exactly: same receiver (&self here), same parameters, same return type — an impl that returns &'static str instead of String is rejected as an incompatible type for the trait, because callers were promised the trait's signature. Second, the method body is free: nothing about Article changes, no field is added, no base class is involved. Rust has no classes, so data lives in the struct, behavior lives in the trait, and impl ... for ... is the seam between them. Third — and this trips every newcomer — the trait must be in scope at the call site:

fn main() {
    let a = Article { title: "x".into(), body: "y".into() };
    println!("{}", a.summarize());
}
error[E0599]: no method named `summarize` found for struct `Article` in the current scope
  |
  = note: items from traits can only be used if the trait is implemented and in scope

Adding use crate::Summary; fixes it. Method resolution searches inherent methods first (those from your impl Article block), then every trait in scope; a trait that is implemented but never imported contributes nothing. That rule is why two traits with the same method name can coexist — and why you sometimes write Summary::summarize(&a) to disambiguate when both are in scope.

Note what this decoupling buys. The trait and the struct can live in different files, different crates, even different decades — you can implement your trait for your type, your trait for a foreign type such as String, or a foreign trait for your type. Only one of those four combinations is banned, and chapter three explains which.

A trait can also declare associated functions without a receiver (fn create() -> Self), called as Article::create() when unambiguous, and it can declare generics. For now the pattern you will use daily is a function that accepts anything implementing the trait:

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

That signature is a promise about behavior, not a concrete type — the machinery that makes it precise is Generic Trait Bounds. Interviewers ask about traits constantly, and the question behind most of them is the same: how do you add behavior to a type you did not write, or to one whose implementation you need to swap in tests? The answer starts with impl Trait for Type.

Default Method Bodies and Supertraits: Sharing Behavior Without a Base Class

A trait where every method must be implemented by every type gets tedious fast, and it makes evolving a public trait dangerous. Default method bodies solve both:

pub trait Summary {
    fn summarize(&self) -> String;                       // required: no body

    fn preview(&self, width: usize) -> String {          // defaulted: inherit it
        let full = self.summarize();          // a default may call required methods
        if full.chars().count() > width {
            format!("{}…", full.chars().take(width).collect::<String>())
        } else {
            full
        }
    }
}

impl Summary for Video {
    fn summarize(&self) -> String { format!("{} ({}s)", self.title, self.seconds) }
    // preview() inherited as written — no code duplicated
}

Implementors choose per method: inherit the default, or override it. A default body may freely call the required methods, because every implementor is guaranteed to have them — that is the template-method shape: implement the one primitive (summarize), get a family of derived methods for free. Two limits are worth knowing. A default cannot invent a Self value — there is no Self { .. } literal and no Self::new() unless a supertrait guarantees a constructor — because the default code is shared by every implementor and does not know which one it is running for. And a default body is part of the trait's compiled interface: changing its logic changes behavior for every type that inherits it, at dependency-update time, with no edit in their code.

Why defaults matter most is backward compatibility. std::iter::Iterator is the proof: it has exactly one required method, next(), and dozens of defaults (for_each, copied, flatten, sum, enumerate) layered on over the years. Adding for_each in Rust 1.35 broke nobody, because no implementor had to write it. Add a required method to a public trait and every implementor in the ecosystem fails to compile — which is why library authors reach for a default body first, and why "can this new method be a default?" is a real design review question.

Supertraits are the other composition tool, and they run in the opposite direction:

pub trait Notify {
    fn notify(&self);
}

pub trait Task: Notify {          // every Task is also a Notify
    fn run(&self);
}

impl Notify for Video {
    fn notify(&self) { println!("done: {}", self.title); }
}

impl Task for Video {
    fn run(&self) { println!("running {}", self.title); }
}

fn dispatch<T: Task>(item: &T) {
    item.notify();     // legal: `T: Task` implies `T: Notify`
    item.run();
}

Declaring Task: Notify means the compiler treats the bound T: Task as carrying T: Notify as well — you never write both at a call site. Skip the Notify impl and the trait declaration itself rejects the code:

error[E0277]: the trait bound `Video: Notify` is not satisfied
  |
  = help: the trait `Notify` is not implemented for `Video`
  = note: required by a bound in `Task`

There is no base class to inherit from and no runtime check to pay for — the requirement is written in the trait header and proven at compile time.

Supertraits are also the reason a trait hierarchy must be considered before reaching for dyn: a supertrait that cannot become a trait object blocks the subtrait too, because the object's table must expose the supertrait's methods as well. That is the subject of Trait Objects and Dynamic Dispatch.

One more inheritance-flavored feature exists purely for tests and wrappers: a blanket impl<T: Summary> Summary for Wrapper<T> forwards to the inner value. It is legal because Summary is your trait — which brings up the rule everyone hits eventually.

The Orphan Rule: Why You Cannot Implement a Foreign Trait for a Foreign Type

You want Vec<u8> to print nicely, so you write impl std::fmt::Display for Vec<u8> in your crate. The compiler refuses:

error[E0117]: only traits defined in the current crate can be implemented for
types defined outside of the crate
  |
4 | impl std::fmt::Display for Vec<u8> {
  | ^^^^^-----------------------------
  | impl doesn't use only types from inside the current crate
  = note: upstream crates may add a new impl of trait `std::fmt::Display`
          for type `std::vec::Vec<u8>` in future versions

This is the orphan rule, and it is coherence, not conservatism. The rule: in impl<...> Trait for Type, at least one of the trait or the type must be local to your crate, and generic parameters must be covered by a local type.

Trait Type Allowed? Why
local local yes your crate, your rules
local foreign (impl Summary for String) yes you own the contract
foreign (impl Display for User) local yes you own the type
foreign foreign no E0117

Why the fourth row is fatal: suppose it were allowed. Your crate implements Display for Vec<u8>; next month the standard library adds its own; a third crate depends on both. Method resolution for write!(f, "{}", bytes) would have two candidate impls and no principled way to pick one — monomorphization would generate different code depending on dependency order. Rust refuses the ambiguity up front, and the note in the error is the giveaway: the real problem is that the upstream crate must remain free to add its own impl later without breaking yours.

The generic-parameter clause is the subtler half. impl<T> Display for MyVec<T> passes, because MyVec is local and covers T. But a bare type parameter never counts as local: impl<T> Display for T is rejected with E0210 (type parameter T must be covered by another type when they appear in the input types of a trait impl) — otherwise anyone's blanket impl would collide with everyone else's.

Two consequences show up in review. You cannot "specialize" a foreign trait for a subset of foreign types either — impl fmt::Display for Vec<T> where T: MyTrait is still foreign-plus-foreign, and a where clause does not make the type local. And note the direction of the fix: your traits stay open to the world, because implementing a local trait for any type — foreign or not — is always allowed. That asymmetry is why extensible Rust libraries publish traits rather than abstract base classes: downstream crates extend the interface themselves without asking you.

When the rule blocks you, the newtype is the standard escape, and it is exactly the wrapper introduced in Structs and Field Syntax:

use std::fmt;

pub struct ByteList(pub Vec<u8>);      // local type, zero-size wrapper

impl fmt::Display for ByteList {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for b in &self.0 { write!(f, "{b:02x}")?; }
        Ok(())
    }
}

ByteList is your type, so the impl is legal, and the cost is honest: construct with ByteList(bytes) and read with .0 (or add a Deref to smooth reads). The wrapper adds no memory and no branch — the distinction lives entirely in the type system. This pattern shows up everywhere in real crates: serializing a foreign type, adding a local trait bound to HashMap, or giving a foreign error type your crate's formatting. It is also why "implement a foreign trait for a foreign type" is an interview question with a layered answer: state the rule, then the coherence reason, then the newtype fix.

With traits declared, defaulted, and placed, the next step is making the compiler require them: Generic Trait Bounds.

Quiz

1. You call `article.summarize()` on a type that implements `Summary`, and the compiler answers with `error[E0599]: no method named 'summarize' found for struct 'Article' in the current scope`. What is wrong?

Question 1 options

2. In your crate you write `impl std::fmt::Display for Vec<u8>`. What happens, and why?

Question 2 options

3. How has `Iterator` gained dozens of methods (`for_each`, `sum`, `copied`, `flatten`) without breaking every implementor in the ecosystem?

Question 3 options

4. Given `trait Task: Notify`, you write `impl Task for Video` but never `impl Notify for Video`. Which diagnostic do you get?

Question 4 options

Flashcards

Question

What are the two forms for attaching behavior to a type, and what does each mean?

Answer

`impl Type { ... }` adds inherent methods only that type has, and they win method resolution; `impl Trait for Type` attaches a shared contract that any compatible type may implement. A trait carries no data — fields stay in the struct — and because Rust has no classes, there is no inheritance layer between them.

Question

Why does calling a trait method without importing the trait produce E0599, and what are the fixes?

Answer

Method resolution searches inherent impls first, then traits that are *in scope*; an unimported trait never enters the search. Fix with `use crate::Summary;` or the fully qualified `Summary::summarize(&a)`, which is also how you disambiguate when two in-scope traits declare the same method name.

Question

Why do default method bodies matter for a public trait's evolution?

Answer

They make changes additive: a defaulted method compiles for every existing implementor, while a new required method breaks all of them (a semver-breaking change). `Iterator` is the proof — one required method (`next`) plus dozens of defaults layered on over the years.

Question

State the orphan rule and the escape when it blocks you.

Answer

In `impl<...> Trait for Type`, at least one of trait or type must be local to your crate; bare type parameters never count as local (E0210). Foreign trait + foreign type = E0117. Fix: wrap the foreign type in a local newtype and implement for the wrapper — zero runtime cost, full type distinction.

Question

What does a supertrait guarantee, and what must hold for `dyn Sub` to work?

Answer

`trait Task: Notify` means every `Task` implementor must also implement `Notify` (missing impl → E0277, `required by a bound in Task`), and a `T: Task` bound carries `Notify` without being written twice. Supertraits must be object safe as well, because `dyn Sub` has to expose their methods from the same vtable.

Revision Notes

Key Takeaways

  • 1. A trait is data-free contract; `impl Trait for Type` is the only attachment mechanism — data stays in the struct, behavior in the trait, no inheritance layer between them
  • 2. Trait methods must be in scope at the call site: missing `use` produces E0599, and inherent methods always resolve before trait methods
  • 3. Default bodies make public traits evolvable — `Iterator` grew dozens of methods without breaking implementors, because only `next()` is required
  • 4. Supertraits (`trait Task: Notify`) compose interfaces statically: a `T: Task` bound carries `Notify`, and both impls must exist
  • 5. The orphan rule (E0117) bans foreign-trait-for-foreign-type impls to keep method resolution coherent; the newtype wrapper is the sanctioned escape

Interview Tips

  • • "\"How do you add behavior to a type you don't own?\" — implement one of your traits for it, or wrap it in a newtype and implement the foreign trait for the wrapper; naming E0117 and the coherence reason shows you understand *why*, not just *what* fails"
  • • "\"Why does E0599 say the trait must be in scope?\" — method resolution is inherent-methods-first, then imported traits; explain the fix (`use`) and the disambiguation form (`Summary::summarize(&a)`) when two traits collide"
  • • "\"Why does Iterator only require `next()`?\" — every other method is a default built on it, which is how a public trait stays backward compatible; contrast with adding a required method, which breaks the ecosystem"
  • • "\"What is the orphan rule?\" — give the table in two sentences (local trait OR local type), then the coherence argument about a downstream crate seeing two competing impls, then the newtype fix"

Cheat Sheet

Traits and Implementations

Declare and implement:

pub trait Summary {
    fn summarize(&self) -> String;                 // required
    fn preview(&self, w: usize) -> String { /* default, may call summarize() */ }
}

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

Facts to keep straight:

Fact Detail
Attachment impl Trait for Type — the only way; there are no classes or inheritance
Signature receiver, params, and return type must match the trait exactly
Scope trait must be used at the call site — otherwise E0599 with "items from traits can only be used if the trait is implemented and in scope"
Resolution inherent methods win, then traits in scope; disambiguate with Trait::method(x)
Defaults inheritable per method; may call required methods; cannot construct Self
Evolution adding a required method breaks every implementor; a default body is additive (Iterator: next() + dozens of defaults)
Supertraits trait Task: Notify ⇒ T: Task implies T: Notify; both impls required

Orphan rule (coherence): at least one of trait/type must be local; generic params must be covered by a local type.

Trait Type Legal?
local foreign ✅ your contract
foreign local ✅ your type
foreign foreign ❌ E0117 — escape with a newtype: struct Wrapper(Vec<u8>);

Scope checklist: use the trait · match the signature · implement supertraits · keep impls coherent.

Continue Learning

Before You Start

Next Topics

Related Reading