Chapter 14

Traits

👋 Anyone can read and edit this exercise. Sign up to save your progress.

A trait is Rust's word for "a named collection of method signatures that any type can opt into." If you've used Java or C# interfaces, C++ abstract classes with pure virtual methods, Haskell type classes, Swift protocols, or Python's abc/Protocol, you already know the gist of it.

The Rust spelling is:

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

struct English;
struct German;

impl Greet for English {
    fn hello(&self) -> String { "Hello!".to_string() }
}

impl Greet for German {
    fn hello(&self) -> String { "Hallo!".to_string() }
}

English and German have nothing in common structurally, but both "implement Greet." Anywhere code asks for a Greet, either will do.

You've actually been using traits since Chapter 6. Every time you wrote #[derive(Debug, PartialEq)] on an enum or struct, you were asking the compiler to write the impl Debug for ... and impl PartialEq for ... blocks for you. That's all derive is: a macro that emits the obvious implementation so you don't have to type it out. We'll revisit this in a moment.

What's in this chapter

  1. Implementing an existing trait. You'll write impl Display for Temperature so a value formats itself as "21.5°C" in println! and format!.
  2. Defining your own trait. A Describable trait with two implementations, plus a generic function with a trait bound that accepts any T: Describable.
  3. Default methods. Traits can ship a default body for a method, which implementors inherit unless they override it. This is where traits start to feel like mixins.
  4. Trait objects (dyn Trait). Generics give you one specialized copy of a function per concrete type ("static dispatch"). Trait objects give you one function that dispatches at runtime ("dynamic dispatch") and let you put different types into the same Vec.

A quick map of stdlib traits you've already met

TraitWhat it gives youFirst seen
Debug{:?} formattingChapter 6 (enums)
Display{} formattingthis chapter
PartialEq, Eq== and !=Chapter 6
Clone, Copy.clone() and implicit copiesChapter 12
DefaultT::default()mentioned in passing
Iteratorfor x in iter, all the combinatorsChapter 16
From, IntoT::from(x) and x.into() conversionssprinkled throughout

None of those are magic. Each is a regular trait defined in std, and the standard library impls it for the obvious built-in types. When you derive one, the compiler writes the impl. When you can't derive (maybe the auto-generated version isn't what you want), you write impl Display for MyType { ... } by hand, just like in step 2.

Static vs. dynamic dispatch: a sneak preview

// Static dispatch: the compiler generates a specialized copy of
// `print_all` for each `T` you use it with. Zero runtime cost,
// but every `T` in one call must be the same concrete type.
fn print_all<T: Display>(items: &[T]) { /* ... */ }

// Dynamic dispatch: one function, one vtable lookup per call.
// The slice can mix different concrete types that all implement
// `Display`.
fn print_all_dyn(items: &[&dyn Display]) { /* ... */ }

You'll write both in this chapter. The Box<dyn Trait> form, which solves the "but trait objects don't have a known size" problem you'll bump into, is the headline act of Chapter 15 on smart pointers.

Implementing `Display` for your own type

Display is the trait behind the {} placeholder in println!, format!, and friends. Implementing it for your struct means values of that struct can be formatted as user-facing text the same way a number or a String can.

The trait lives in std::fmt and looks like this:

pub trait Display {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}

Don't be put off by the signature. In practice you write a one-liner that delegates to the write! macro, which has the same template syntax as println! but writes into the formatter:

use std::fmt;

struct Pixel(u8, u8, u8);

impl fmt::Display for Pixel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{:02X}{:02X}{:02X}", self.0, self.1, self.2)
    }
}

After that, format!("{}", Pixel(255, 0, 0)) produces "#FF0000".

Why isn't there a #[derive(Display)]?

Because there's no obvious default. Debug has one (print the type name and fields), but Display is for human-readable output and only you know what that should look like for your type. So you write it by hand. The Java/C# parallel is overriding toString(); the Python one is __str__.

Useful from the standard library

  • std::fmt::Display is the trait. use std::fmt; and then impl fmt::Display for T is the idiomatic spelling.
  • write! is the formatter-targeted cousin of println!. It returns std::fmt::Result, which is exactly what your fmt method needs to return, so a single write!(...) call is usually the whole body.
  • Format specifiers carry over: {:.1} rounds a float to one decimal place, so format!("{:.1}", 21.5_f64) is "21.5". You'll want that for the temperature output.
Exercise 1 of 4
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Stuck? Show a hint No spoilers, just a nudge
    1. The whole fmt body is one write! call.
    2. write! takes the formatter, then a format string, then the args: write!(f, "{:.1}°C", self.celsius).
    3. Don't forget the return: write! already returns fmt::Result, so its result is your return value. No semicolon on the last line, or use an explicit return.
    4. impl fmt::Display for Temperature {
          fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
              write!(f, "{:.1}°C", self.celsius)
          }
      }
      

    Defining your own trait

    Now you're on the other side of the contract. Instead of impling a trait someone else wrote, you'll write the trait yourself, give two types their own implementation, and then write a generic function that accepts anything implementing it.

    trait Describable {
        fn describe(&self) -> String;
    }
    

    That's the entire interface. Any type can opt in by writing impl Describable for MyType { fn describe(&self) -> String { ... } }.

    Trait bounds on generics

    Once a trait exists, you can use it as a bound on a generic parameter to say "I accept any T, as long as T implements this trait":

    fn print_one<T: Describable>(item: &T) {
        println!("{}", item.describe());
    }
    

    This is Rust's answer to "polymorphism." The compiler stamps out one specialized copy of print_one per type you call it with (called monomorphization; the C++ template crowd will feel at home). There's no runtime dispatch and no boxing.

    A few variations you'll meet in real code, just so you recognize them:

    // Multiple bounds with `+`:
    fn show<T: Describable + std::fmt::Debug>(item: &T) { /* ... */ }
    
    // Same thing, written with a `where` clause. Easier to read once you
    // have several parameters or long bounds:
    fn show<T>(item: &T)
    where
        T: Describable + std::fmt::Debug,
    {
        /* ... */
    }
    
    // `impl Trait` in argument position is shorthand for a single
    // unnamed generic parameter:
    fn show(item: &impl Describable) { /* ... */ }
    

    You'll write the simple <T: Describable> form in this step. The others are sugar for the same underlying machinery.

    Useful from the standard library

    • The Rust Book on traits walks through definitions, implementations, and bounds with more examples than fit here.
    • Vec<String>::join("\n") (and any &[String].join(...)) is handy for the print_descriptions exercise: build a Vec<String> of per-item descriptions, then join them with newlines.
    • The standard Iterator::map plus .collect::<Vec<_>>() is the idiomatic way to turn a &[T] into a Vec<String>. You met both in passing; Chapter 16 covers iterators properly.
    Exercise 2 of 4
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge
      1. Two describe methods are one-line format! calls:
        • format!("{} by {}", self.title, self.author) for Book.
        • format!("{} ({})", self.title, self.year) for Movie.
      2. For print_descriptions, build a Vec<String> and join it:
        let lines: Vec<String> = items.iter().map(|x| x.describe()).collect();
        lines.join("\n")
        
      3. If you're not comfortable with .map/.collect yet (chapter 16 covers them properly), a plain for loop works just as well:
        let mut lines: Vec<String> = Vec::new();
        for item in items {
            lines.push(item.describe());
        }
        lines.join("\n")
        

      Default methods

      A trait method can ship with a default body. Implementors get that method for free, but any one of them can override it if the default doesn't fit.

      trait Greet {
          fn name(&self) -> &str;
      
          // Default body. Implementors get this for free.
          fn hello(&self) -> String {
              format!("Hello, {}!", self.name())
          }
      }
      

      A type that says impl Greet for X { fn name(&self) -> &str { "world" } } automatically has .hello() available, returning "Hello, world!", without writing it out. Provide a fn hello in the impl block and yours wins instead.

      This is how Iterator gets away with offering dozens of methods (map, filter, sum, count, ...) while only requiring you to implement one: fn next(&mut self) -> Option<Self::Item>. Every other method is a default body written in terms of next. Haskellers will recognise the pattern from type class default methods; Java added the same feature as "default methods on interfaces" in Java 8.

      What this step adds

      You'll work with a small Logger trait. There's exactly one required method (log, which formats a single line) and two default methods (warn and error) that build on top of it.

      trait Logger {
          fn log(&self, msg: &str) -> String;
      
          fn warn(&self, msg: &str) -> String {
              self.log(&format!("[WARN] {msg}"))
          }
      
          fn error(&self, msg: &str) -> String {
              self.log(&format!("[ERROR] {msg}"))
          }
      }
      

      This is deliberately recognisable: every logging library in every language has this same skeleton. The teaching value of the default methods is that they live on the trait, so adding a new implementor doesn't mean writing warn and error from scratch every time.

      Two implementors:

      1. PlainLogger returns the message untouched. It uses both defaults as written, so all you have to write is log.
      2. TaggedLogger { tag: String } prepends a tag (e.g. "auth: ..."). It uses the default warn, but overrides error to swap the [ERROR] prefix for a louder [CRITICAL] prefix.

      That asymmetry is the point: implementors take what they want from the defaults and override only the bits they need to change.

      Useful from the standard library

      • The format! macro is the workhorse here. Default warn builds "[WARN] {msg}" and hands it back to self.log, so whatever decoration log does (the tag, in TaggedLogger's case) wraps the warning prefix.
      • Default methods are written inside the trait block, with a body instead of a trailing semicolon. The required methods (the ones without a body) are still mandatory; defaults are bonus.
      Exercise 3 of 4
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge
        1. PlainLogger::log is one line: msg.to_string(). That's all. Do not write warn or error for PlainLogger; the defaults already do the right thing.
        2. TaggedLogger::log is also one line: format!("{}: {}", self.tag, msg). The default warn calls this log, so warn("slow") ends up as "auth: [WARN] slow" automatically.
        3. TaggedLogger::error is the override. The pattern mirrors the default body, just with a different prefix:
          fn error(&self, msg: &str) -> String {
              self.log(&format!("[CRITICAL] {msg}"))
          }
          
        4. Notice the symmetry with object-oriented "inheritance": PlainLogger inherits both defaults, TaggedLogger inherits one and overrides the other. The trait is the only place a default ever lives, so there's no surprise about where behavior comes from.

        Trait objects: `dyn Trait`

        The generic print_descriptions<T: Describable> from step 3 is fast and zero-cost, but it has one limit: every element of a single call must be the same concrete T. You can pass &[Book] or &[Movie], but not a slice that contains both.

        That's because the compiler picks one T per call site and produces a specialized copy of the function for it. The slice type &[T] has to agree on a single element type, and two different structs are two different types as far as the type system is concerned.

        Enter dyn Trait

        When you want a single collection that holds different concrete types as long as they all implement the same trait, you reach for a trait object, spelled dyn Trait:

        fn run_all(items: &[&dyn Validator], input: &str) {
            for v in items {
                let _ = v.check(input);
            }
        }
        

        Two things changed compared to the generic version:

        1. Element type. &dyn Validator is a fat pointer: two words that together point at the value and at a vtable of function pointers (one per trait method). At each call to .check(...), Rust looks the function up in the vtable. C++ folks: this is the same machinery as virtual methods, just opt-in per call site instead of per class.
        2. One function, not many. run_all is compiled exactly once. The price is the indirection through the vtable; the payoff is heterogeneous collections.

        Static vs. dynamic dispatch, side by side

        fn f<T: Trait>(x: &T)fn f(x: &dyn Trait)
        Dispatchstatic, decided at compile timedynamic, vtable lookup at runtime
        Code sizeone copy per T you useone copy total
        Mixed collectionsnoyes
        Runtime costnoneone indirect call per trait-method call

        Neither is "better." Use generics by default for performance and flexibility, and reach for dyn Trait when you genuinely need heterogeneous storage or want a smaller binary.

        What this step adds

        You'll build a tiny composable validation library. The trait is deliberately small:

        trait Validator {
            /// `Ok(())` on success, `Err(message)` on failure.
            fn check(&self, input: &str) -> Result<(), String>;
        }
        

        Three implementors, each enforcing one rule:

        The killer feature is that MinLength, MustContain, and MustNotContain are three different types, but a single &[&dyn Validator] slice can hold all of them at once. That's what makes "pluggable rules" work: every rule is a different struct (and might carry different configuration), but the call site only knows "a list of things that can validate."

        You'll see the same idea, scaled up, in Chapter 18's password validator: a configurable set of checks running against one input.

        A word about Box<dyn Trait>

        You'll see Box<dyn Trait> everywhere in real Rust code:

        let rules: Vec<Box<dyn Validator>> = vec![
            Box::new(MinLength { n: 8 }),
            Box::new(MustContain { needle: "@".to_string() }),
        ];
        

        The reason: dyn Trait has no statically known size (the three implementors above can carry different fields, so they don't all take up the same number of bytes), so the compiler won't let you put bare dyn Validator values directly in a Vec. A Box is a heap allocation with a fixed-size pointer that lives on the stack, which sidesteps the size problem. That's exactly what Chapter 15 on smart pointers unpacks. For this step we sidestep it with &dyn Validator references, which are also fixed-size and work fine for borrowing.

        Useful from the standard library

        • The Rust Book on trait objects.
        • str::contains (with a &str argument) is all you need for the MustContain / MustNotContain checks.
        • Inside collect_errors, a plain for loop pushing into a Vec<String> is the most direct form. The same chain with .iter().filter_map(...) works once you've met iterators in Chapter 16.
        Exercise 4 of 4
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge
          1. The two missing check impls follow MinLength exactly. Use input.contains(&self.needle) and the inverse:
            if !input.contains(&self.needle) {
                Err(format!("must contain '{}'", self.needle))
            } else {
                Ok(())
            }
            
            and:
            if input.contains(&self.forbidden) {
                Err(format!("must not contain '{}'", self.forbidden))
            } else {
                Ok(())
            }
            
          2. For collect_errors, a plain for loop is the most readable:
            let mut errors = Vec::new();
            for v in validators {
                if let Err(msg) = v.check(input) {
                    errors.push(msg);
                }
            }
            errors
            
          3. The slice element type &dyn Validator means each v in the loop is a &&dyn Validator. Method calls auto-deref, so v.check(input) works without ceremony.
          4. Once you've met iterators (chapter 16), the same body collapses to validators.iter().filter_map(|v| v.check(input).err()).collect().

          Wrapping up traits

          You implemented an stdlib trait (Display), defined your own (Describable), wrote a generic function with a trait bound, gave a trait a default method that one type inherited and another overrode (the Logger step), and finally swapped a generic for a trait object so a single slice could hold a mix of validation rules.

          What we learned

          • A trait is a named collection of method signatures. Any type can opt in with impl TraitName for TypeName { ... }. Same idea as Java/C# interfaces, Haskell type classes, Swift protocols, or C++ abstract classes with pure virtual methods.
          • #[derive(...)] is sugar: the compiler writes the obvious impl Trait for Type block for you. Debug, PartialEq, Eq, Clone, Copy, Default, Hash, and Ord are the everyday derivable ones. Display is not derivable because there's no one-size-fits-all human-readable format.
          • impl Display is the toString / __str__ of Rust. Implement fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result with a single write!(...) call.
          • Trait bounds on a generic say "I accept any T that implements this trait." fn f<T: Trait>(x: &T) is the basic form, T: A + B combines bounds, and where clauses let you push long bounds out of the signature.
          • Default methods in the trait body give every implementor a baseline behavior. Override per type when you need to.
          • Generics dispatch statically: one specialized copy of the function per concrete T. Fast, but one call can only see one concrete type. Great default.
          • Trait objects (dyn Trait) dispatch dynamically through a vtable. Slower per call but lets one slice or Vec hold values of multiple concrete types at once. Reach for them when you need heterogeneity.
          • Box<dyn Trait> solves the "trait objects have no known size" problem so they can live in owning containers like Vec. That's the headline of Chapter 15 on smart pointers.
          • Many stdlib traits you already use (Iterator, From, Into, PartialEq, ...) are just regular traits. Nothing magic. You can define your own or implement the standard ones for your own types.
          Next chapter 15Smart Pointers