Chapter 17

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 the enums chapter. 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.

From familiar traits to trait objects

The first exercise implements Display, a standard library trait, for a temperature type. Describable provides a bound for a generic function. Default methods share behavior between implementations without repetition. dyn Trait lets one collection hold values of different concrete types.

Standard library traits you've already met

TraitWhat it gives youWhere you know it from
Debug{:?} formattingenums
Display{} formattingthe exercises below
PartialEq, Eq== and !=enums
Clone, Copy.clone() and implicit copiesstructs and methods
DefaultT::default()earlier mentions
Iteratorfor x in iter, all the combinatorsiterator pipelines in later exercises
From, IntoT::from(x) and x.into() conversionsearlier conversions

None of those are magic. Each is a regular trait defined in std, with implementations for the built-in types where they make sense. When you derive one, the compiler writes the implementation. When the generated behavior isn't what you want, you write the implementation by hand.

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 use both forms in the exercises below. For ownership, Box<dyn Trait> gives an unsized trait object a fixed-size handle.

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.
    Reveal the full solution Spoiler: the complete answer
    use std::fmt;
    
    #[derive(Debug, PartialEq)]
    struct Temperature {
        celsius: f64,
    }
    
    /// Formats the temperature as `"<value>°C"` with one decimal place.
    /// Examples:
    ///   - `Temperature { celsius: 21.5 }`  → `"21.5°C"`
    ///   - `Temperature { celsius: -3.0 }`  → `"-3.0°C"`
    ///   - `Temperature { celsius: 100.0 }` → `"100.0°C"`
    impl fmt::Display for Temperature {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "{:.1}°C", self.celsius)
        }
    }
    
    #[test]
    fn test_display_room_temperature() {
        let t = Temperature { celsius: 21.5 };
        assert_eq!(format!("{t}"), "21.5°C");
    }
    
    #[test]
    fn test_display_negative() {
        let t = Temperature { celsius: -3.0 };
        assert_eq!(format!("{t}"), "-3.0°C");
    }
    
    #[test]
    fn test_display_boiling() {
        let t = Temperature { celsius: 100.0 };
        assert_eq!(format!("{t}"), "100.0°C");
    }
    
    #[test]
    fn test_display_rounds_to_one_decimal() {
        // `{:.1}` rounds, it doesn't truncate.
        let t = Temperature { celsius: 18.27 };
        assert_eq!(format!("{t}"), "18.3°C");
    }
    

    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.

    Real code often spells the same kind of bound in one of these forms:

    // 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) { /* ... */ }
    

    For this exercise, use the simple <T: Describable> form. You only need to recognize the others as different spellings of the same 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>. Iterator::map and collect appear throughout the iterator exercises.
    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 (the iterators chapter 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")
        
      Reveal the full solution Spoiler: the complete answer
      /// A type that knows how to describe itself in one short line.
      trait Describable {
          fn describe(&self) -> String;
      }
      
      #[derive(Debug)]
      struct Book {
          title: String,
          author: String,
      }
      
      #[derive(Debug)]
      struct Movie {
          title: String,
          year: u16,
      }
      
      /// Implement `Describable` for `Book` so that
      /// `Book { title: "Dune".into(), author: "Herbert".into() }.describe()`
      /// returns `"Dune by Herbert"`.
      impl Describable for Book {
          fn describe(&self) -> String {
              format!("{} by {}", self.title, self.author)
          }
      }
      
      /// Implement `Describable` for `Movie` so that
      /// `Movie { title: "Arrival".into(), year: 2016 }.describe()`
      /// returns `"Arrival (2016)"`.
      impl Describable for Movie {
          fn describe(&self) -> String {
              format!("{} ({})", self.title, self.year)
          }
      }
      
      /// Generic function with a trait bound.
      ///
      /// Accepts a slice of *any* type `T` that implements `Describable`,
      /// calls `.describe()` on each element, and joins the results with a
      /// newline (`"\n"`) between them. An empty slice returns `""`.
      ///
      /// Note: because of the `T: Describable` bound, every element of one
      /// call must be the same concrete type. Mixing `Book`s and `Movie`s
      /// in the same call needs trait objects, which is the next step.
      fn print_descriptions<T: Describable>(items: &[T]) -> String {
          let mut lines = Vec::new();
          for item in items {
              lines.push(item.describe());
          }
          lines.join("\n")
      }
      
      #[test]
      fn test_book_describe() {
          let b = Book {
              title: "Dune".to_string(),
              author: "Herbert".to_string(),
          };
          assert_eq!(b.describe(), "Dune by Herbert");
      }
      
      #[test]
      fn test_movie_describe() {
          let m = Movie {
              title: "Arrival".to_string(),
              year: 2016,
          };
          assert_eq!(m.describe(), "Arrival (2016)");
      }
      
      #[test]
      fn test_print_descriptions_books() {
          let library = [
              Book {
                  title: "Dune".to_string(),
                  author: "Herbert".to_string(),
              },
              Book {
                  title: "Hyperion".to_string(),
                  author: "Simmons".to_string(),
              },
          ];
          assert_eq!(
              print_descriptions(&library),
              "Dune by Herbert\nHyperion by Simmons"
          );
      }
      
      #[test]
      fn test_print_descriptions_movies() {
          let films = [Movie {
              title: "Arrival".to_string(),
              year: 2016,
          }];
          assert_eq!(print_descriptions(&films), "Arrival (2016)");
      }
      
      #[test]
      fn test_print_descriptions_empty() {
          let films: [Movie; 0] = [];
          assert_eq!(print_descriptions(&films), "");
      }
      

      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.

      A logger with shared behavior

      The Logger trait has one required method, log, which formats a single line. Its warn and error methods provide default bodies built on top of log.

      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}"))
          }
      }
      

      The shape should look familiar from logging APIs in other languages. Because warn and error live on the trait, each new implementor gets both without writing them again.

      You'll implement that contrast with two types:

      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 lets each implementor keep the defaults that fit and replace only the behavior that differs.

      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. A method ending in a semicolon remains required, while a method with a body can be inherited or overridden.
      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.
        Reveal the full solution Spoiler: the complete answer
        /// A small `Logger` trait. One required method (`log`), plus two
        /// default methods that build on it (`warn` and `error`).
        ///
        /// Implementors only have to write `log`; they get `warn` and
        /// `error` for free unless they explicitly override them.
        trait Logger {
            /// Required: turn a message into the final log line.
            fn log(&self, msg: &str) -> String;
        
            /// Default: prepend `"[WARN] "` and forward through `log`.
            fn warn(&self, msg: &str) -> String {
                self.log(&format!("[WARN] {msg}"))
            }
        
            /// Default: prepend `"[ERROR] "` and forward through `log`.
            fn error(&self, msg: &str) -> String {
                self.log(&format!("[ERROR] {msg}"))
            }
        }
        
        /// A logger that returns the message untouched. It should use the
        /// default `warn` and `error` (do *not* write them in this impl).
        struct PlainLogger;
        
        impl Logger for PlainLogger {
            /// Return `msg` as a `String`, with nothing added.
            fn log(&self, msg: &str) -> String {
                msg.to_string()
            }
        }
        
        /// A logger that prepends a tag, like `"auth: something went wrong"`.
        ///
        /// It uses the default `warn` (so warnings come out as
        /// `"auth: [WARN] ..."`), but *overrides* `error` to use a louder
        /// `[CRITICAL]` prefix instead of the default `[ERROR]`.
        struct TaggedLogger {
            tag: String,
        }
        
        impl Logger for TaggedLogger {
            /// Return `"{tag}: {msg}"`.
            fn log(&self, msg: &str) -> String {
                format!("{}: {}", self.tag, msg)
            }
        
            /// Override: build a `[CRITICAL]`-prefixed message and forward
            /// it through `log` (so the tag still wraps the result).
            /// Expected output for `TaggedLogger { tag: "auth" }.error("nope")`
            /// is `"auth: [CRITICAL] nope"`.
            fn error(&self, msg: &str) -> String {
                self.log(&format!("[CRITICAL] {msg}"))
            }
        }
        
        #[test]
        fn plain_logger_log_is_passthrough() {
            let l = PlainLogger;
            assert_eq!(l.log("ready"), "ready");
        }
        
        #[test]
        fn plain_logger_inherits_warn() {
            let l = PlainLogger;
            assert_eq!(l.warn("slow query"), "[WARN] slow query");
        }
        
        #[test]
        fn plain_logger_inherits_error() {
            let l = PlainLogger;
            assert_eq!(l.error("disk full"), "[ERROR] disk full");
        }
        
        #[test]
        fn tagged_logger_prepends_tag() {
            let l = TaggedLogger {
                tag: "auth".to_string(),
            };
            assert_eq!(l.log("ok"), "auth: ok");
        }
        
        #[test]
        fn tagged_logger_inherits_warn_via_its_own_log() {
            // Default warn calls self.log, so the tag wraps the [WARN] prefix.
            let l = TaggedLogger {
                tag: "auth".to_string(),
            };
            assert_eq!(l.warn("slow"), "auth: [WARN] slow");
        }
        
        #[test]
        fn tagged_logger_overrides_error_with_critical() {
            let l = TaggedLogger {
                tag: "auth".to_string(),
            };
            assert_eq!(l.error("nope"), "auth: [CRITICAL] nope");
        }
        

        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.

        Using 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);
            }
        }
        

        &dyn Validator is a fat pointer: two words that point to the value and to a vtable of function pointers, one for each trait method. At each call to .check(...), Rust looks up the function in that vtable. C++ folks will recognize the same machinery as virtual methods, but here you opt into it at the call site instead of on the class. run_all is also compiled exactly once rather than once per concrete type. That trades one vtable lookup per call for the ability to mix concrete types in the slice.

        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.

        A validation example

        You'll use trait objects to build a small validation library. Each validator needs one method:

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

        You'll give three structs one rule each:

        MinLength, MustContain, and MustNotContain are different types, but one &[&dyn Validator] slice can hold all three. Each rule can carry its own configuration while the call site only needs a list of values that can validate.

        If you take the optional password validator later, you'll use the same idea for a configurable set of checks.

        A word about Box<dyn Trait>

        You'll also see Box<dyn Trait> in 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. Box<dyn Trait> is the owning form of this fixed-size handle. &dyn Validator borrows through a fixed-size handle instead of taking ownership.

        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. An .iter().filter_map(...) chain expresses the same loop with iterator adapters.
        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 (the iterators chapter), the same body collapses to validators.iter().filter_map(|v| v.check(input).err()).collect().
          Reveal the full solution Spoiler: the complete answer
          /// A composable validation rule. `Ok(())` means the input is fine
          /// for this rule; `Err(message)` explains what's wrong.
          trait Validator {
              fn check(&self, input: &str) -> Result<(), String>;
          }
          
          /// Rule: the input must be at least `n` characters long.
          ///
          /// Already implemented for you as the worked example. Read it,
          /// then write the other two impls in the same way.
          struct MinLength {
              n: usize,
          }
          
          impl Validator for MinLength {
              fn check(&self, input: &str) -> Result<(), String> {
                  if input.chars().count() < self.n {
                      Err(format!("must be at least {} characters", self.n))
                  } else {
                      Ok(())
                  }
              }
          }
          
          /// Rule: the input must contain `needle` as a substring.
          ///
          /// On failure return `Err(format!("must contain '{}'", self.needle))`.
          struct MustContain {
              needle: String,
          }
          
          impl Validator for MustContain {
              fn check(&self, input: &str) -> Result<(), String> {
                  if input.contains(&self.needle) {
                      Ok(())
                  } else {
                      Err(format!("must contain '{}'", self.needle))
                  }
              }
          }
          
          /// Rule: the input must *not* contain `forbidden` as a substring.
          ///
          /// On failure return `Err(format!("must not contain '{}'", self.forbidden))`.
          struct MustNotContain {
              forbidden: String,
          }
          
          impl Validator for MustNotContain {
              fn check(&self, input: &str) -> Result<(), String> {
                  if input.contains(&self.forbidden) {
                      Err(format!("must not contain '{}'", self.forbidden))
                  } else {
                      Ok(())
                  }
              }
          }
          
          /// Run every validator against `input` and collect the failure
          /// messages in the order the validators appear.
          ///
          /// The slice element type is `&dyn Validator`: a reference to a
          /// trait object. The slice can mix `MinLength`, `MustContain`, and
          /// `MustNotContain` (and any future implementor) freely. That's the
          /// whole point of trait objects.
          ///
          /// An input that passes everything returns an empty `Vec`. The
          /// returned `Vec<String>` contains only the `Err` messages.
          fn collect_errors(validators: &[&dyn Validator], input: &str) -> Vec<String> {
              let mut errors = Vec::new();
              for validator in validators {
                  if let Err(message) = validator.check(input) {
                      errors.push(message);
                  }
              }
              errors
          }
          
          #[test]
          fn min_length_passes_when_long_enough() {
              let v = MinLength { n: 3 };
              assert_eq!(v.check("abcd"), Ok(()));
          }
          
          #[test]
          fn min_length_fails_when_too_short() {
              let v = MinLength { n: 5 };
              assert_eq!(
                  v.check("hi"),
                  Err("must be at least 5 characters".to_string())
              );
          }
          
          #[test]
          fn must_contain_passes() {
              let v = MustContain {
                  needle: "@".to_string(),
              };
              assert_eq!(v.check("alice@example.com"), Ok(()));
          }
          
          #[test]
          fn must_contain_fails() {
              let v = MustContain {
                  needle: "@".to_string(),
              };
              assert_eq!(v.check("alice"), Err("must contain '@'".to_string()));
          }
          
          #[test]
          fn must_not_contain_passes() {
              let v = MustNotContain {
                  forbidden: " ".to_string(),
              };
              assert_eq!(v.check("no-spaces"), Ok(()));
          }
          
          #[test]
          fn must_not_contain_fails() {
              let v = MustNotContain {
                  forbidden: " ".to_string(),
              };
              assert_eq!(
                  v.check("has a space"),
                  Err("must not contain ' '".to_string())
              );
          }
          
          #[test]
          fn collect_errors_on_clean_input() {
              let r1 = MinLength { n: 3 };
              let r2 = MustContain {
                  needle: "@".to_string(),
              };
              let rules: Vec<&dyn Validator> = vec![&r1, &r2];
              assert!(collect_errors(&rules, "a@b").is_empty());
          }
          
          #[test]
          fn collect_errors_reports_all_failures_in_order() {
              // Three different concrete types, one slice. That's the trait
              // object payoff: pluggable rules, none of which know about each
              // other.
              let r1 = MinLength { n: 8 };
              let r2 = MustContain {
                  needle: "@".to_string(),
              };
              let r3 = MustNotContain {
                  forbidden: " ".to_string(),
              };
              let rules: Vec<&dyn Validator> = vec![&r1, &r2, &r3];
              // Input must trip all three rules:
              //   - shorter than 8 chars (fails MinLength)
              //   - no '@' anywhere   (fails MustContain)
              //   - contains a space  (fails MustNotContain)
              assert_eq!(
                  collect_errors(&rules, "a b"),
                  vec![
                      "must be at least 8 characters".to_string(),
                      "must contain '@'".to_string(),
                      "must not contain ' '".to_string(),
                  ]
              );
          }
          
          #[test]
          fn collect_errors_with_no_rules_returns_empty() {
              let rules: Vec<&dyn Validator> = vec![];
              assert!(collect_errors(&rules, "anything").is_empty());
          }
          

          Wrapping up traits

          You implemented the standard library's Display trait and defined a Describable trait of your own. You used Describable as a generic bound, then shared behavior through default methods. Finally, you switched from a generic to a trait object so one slice could hold several kinds of validation rule.

          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, so the compiler creates one specialized copy of the function per concrete T. Use them when each call only needs one concrete type.
          • Trait objects (dyn Trait) dispatch dynamically through a vtable. Use them when one slice or Vec needs to hold several concrete types at once.
          • Box<dyn Trait> solves the "trait objects have no known size" problem so they can live in owning containers like Vec. Related ownership patterns with Box, Rc, and RefCell appear in the optional smart pointers material.
          • Many standard library traits you already use (Iterator, From, Into, PartialEq, ...) follow the same rules. You can define traits of your own or implement standard traits for your own types.
          Next chapter 18Iterators