Chapter 17

Traits

A struct tries flirting at a bar.
“You’ve got no class, and your methods need work.”
“Maybe. But I’ve got some redeeming traits.”

A trait lets you give unrelated types a shared interface without putting them in a class hierarchy. You declare a named collection of method signatures, and each type can opt in by implementing it. 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 flavor is:

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

struct English;

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

struct German;

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

English and German have nothing in common structurally, but both "implement Greet." Code written against the Greet interface can work with either type.

Standard Library Traits You've Already Met

You've 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.

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

Throughout the exercises, you'll encounter two ways to use traits, called static and dynamic dispatch. Here's what each looks like:

Static Dispatch

With generics, the compiler specializes print_all for the concrete types you use it with. The calls need no trait-object lookup, but every element in one call must have the same concrete type. Many specializations can increase code size and compilation time.

fn print_all<T: Display>(items: &[T]) { /* ... */ }

Dynamic Dispatch

The alternative is to use the dyn keyword. A trait-object reference lets one function call methods on different concrete types through a shared interface. Those calls use indirection, though the optimizer can sometimes remove it. This can reduce the number of specialized functions, but it does not guarantee a smaller binary or faster compilation.

The following slice can mix different concrete types that all implement Display:

fn print_all_dyn(items: &[&dyn Display]) { /* ... */ }

When to Use Each

Use a generic bound when each call works with one concrete type and a trait object when you need to handle different concrete types through the same reference type. Both approaches require implementations known to the program; dyn doesn't remove compile-time checking of the interface.

You'll practice them separately below. First implement traits for individual types, then write a generic caller. Next try inheriting and overriding default methods in separate editors. Finally implement validation rules, borrow a mixed shelf, and collect results from different validators.

Each editor has one job and includes its own support code and tests. Some ask you to write an entire implementation or function, so their first run intentionally reports missing code rather than reaching a todo!(). If you work locally, run one file independently with rustc --edition=2024 --test examples/16_traits/03_describable.rs -o /tmp/traits-test && /tmp/traits-test (change the filename for another step). A chapter-wide cargo test needs every missing definition filled in, even when a test-name filter selects only one step.

Implementing Display for Your Own Type

Display is the trait behind the {} placeholder in println!, format!, and friends. Implement it for your struct, and you can format its values as user-facing text just like a number or a String.

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

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

That signature has a lot of punctuation for a method whose body is often one line. You can delegate to write!, which uses 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.
  • The formatting syntax reference explains how to set floating-point precision.
Exercise 1 of 8
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Stuck? Show a hint No spoilers, just a nudge

    write! takes a formatter followed by a format string and its arguments. It already returns fmt::Result, so you can return that result directly. The precision part of a floating-point format specifier controls how many digits follow the decimal point.

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

    Implement a Shared Interface

    A book and a movie store different information, but both can describe themselves. The Describable trait below defines the interface; your job is to implement it for both types. Write the implementation blocks and methods yourself rather than filling in prewritten bodies.

    Use the fields of the value you receive, not fixed strings from the examples. Descriptions must borrow the value without changing it, so callers can describe the same value again. There is no generic function to write in this step.

    The tests use Describable::describe(&value) to require an actual trait implementation. Writing an unrelated method with the same name won't satisfy that contract. Until you add the implementations, Run will report that the types do not implement Describable. Those compiler errors are the starting point, not broken exercise setup.

    Exercise 2 of 8
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Stuck? Show a hint No spoilers, just a nudge

      Look back at the Greet example in the introduction for the shape of an implementation block. The method signature must match the trait, including its borrowed receiver. Use format! with the current value's fields rather than the test's example text.

      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;
      }
      
      struct Book {
          title: String,
          author: String,
      }
      
      struct Movie {
          title: String,
          year: u16,
      }
      
      impl Describable for Book {
          fn describe(&self) -> String {
              format!("{} by {}", self.title, self.author)
          }
      }
      
      impl Describable for Movie {
          fn describe(&self) -> String {
              format!("{} ({})", self.title, self.year)
          }
      }
      
      #[test]
      fn books_use_their_own_fields() {
          let mut book = Book {
              title: "Dune".into(),
              author: "Herbert".into(),
          };
          assert_eq!(Describable::describe(&book), "Dune by Herbert");
          book.title = "Hyperion".into();
          book.author = "Simmons".into();
          assert_eq!(Describable::describe(&book), "Hyperion by Simmons");
          assert_eq!(book.title, "Hyperion");
      }
      
      #[test]
      fn movies_use_their_own_fields() {
          let mut movie = Movie {
              title: "Arrival".into(),
              year: 2016,
          };
          assert_eq!(Describable::describe(&movie), "Arrival (2016)");
          movie.title = "Spirited Away".into();
          movie.year = 2001;
          assert_eq!(Describable::describe(&movie), "Spirited Away (2001)");
          assert_eq!(movie.title, "Spirited Away");
      }
      

      One Function, Many Types

      Now write a caller that doesn't need to know which concrete type it is describing. A trait bound lets a generic function require an interface without naming every type that might implement it. The compiler checks that requirement at each call.

      Define print_descriptions, including its generic parameter, trait bound, parameter type, and return type. It must borrow a slice of any one type implementing Describable and return an owned String. One call might receive labels; another might receive numbers. It must also work with future implementations of the trait, without requiring extra traits such as Clone or Display.

      Return each element's description in slice order, with one newline between neighboring descriptions and no extra separator at the end. An empty slice returns an empty string. An empty description is still an element: three descriptions "", "middle", and "" produce "\nmiddle\n". Do not print to standard output, consume the slice, or change its elements.

      The tests supply small Label and Number implementations so this editor runs independently of the book/movie exercise. Only your generic function is missing. The initial compiler error about print_descriptions being undefined should disappear once you add it.

      For now, every element in one call has the same concrete type. We'll make a mixed collection later in this chapter.

      Useful from the Standard Library

      • Vec::push can collect owned descriptions from a loop.
      • slice::join combines string elements with a separator, including empty elements.

      You don't need iterator adapters for this exercise; those come in the next chapter.

      Exercise 3 of 8
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Stuck? Show a hint No spoilers, just a nudge

        The type parameter belongs to the function, and its bound names the behavior the body needs. Look back at the static-dispatch example, but use this exercise's trait and return an owned result. A slice parameter borrows its elements, so the bound need not require Clone. Keep an empty description distinct from an empty slice: separators belong between elements, even when an element is empty.

        Reveal the full solution Spoiler: the complete answer
        trait Describable {
            fn describe(&self) -> String;
        }
        
        fn print_descriptions<T: Describable>(items: &[T]) -> String {
            let mut lines = Vec::new();
            for item in items {
                lines.push(item.describe());
            }
            lines.join("\n")
        }
        
        #[cfg(test)]
        mod tests {
            use super::*;
        
            struct Label(String);
            impl Describable for Label {
                fn describe(&self) -> String {
                    self.0.clone()
                }
            }
        
            struct Number(i32);
            impl Describable for Number {
                fn describe(&self) -> String {
                    self.0.to_string()
                }
            }
        
            #[test]
            fn borrows_and_preserves_order() {
                let labels = [Label("second".into()), Label("first".into())];
                assert_eq!(print_descriptions(&labels), "second\nfirst");
                assert_eq!(print_descriptions(&labels), "second\nfirst");
                assert_eq!(labels[0].0, "second");
            }
        
            #[test]
            fn accepts_another_implementor() {
                assert_eq!(print_descriptions(&[Number(42), Number(-7)]), "42\n-7");
            }
        
            #[test]
            fn empty_and_single_item_slices() {
                let empty: [Label; 0] = [];
                assert_eq!(print_descriptions(&empty), "");
                assert_eq!(print_descriptions(&[Number(9)]), "9");
            }
        
            #[test]
            fn empty_descriptions_still_have_separators() {
                let labels = [Label("".into()), Label("middle".into()), Label("".into())];
                assert_eq!(print_descriptions(&labels), "\nmiddle\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.

        This is how Iterator gets away with offering dozens of methods (map, filter, sum, count, ...) while requiring only one method: next. An iterator also declares its item type, which you'll meet in the next chapter. 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. Because they live on the trait, each new implementor gets both without writing them again.

        Implement Logger for PlainLogger so that log returns the message untouched as a String. Write the whole impl block, but only implement the required method. Leave warn and error to the trait: warn("slow query") should return "[WARN] slow query", and error("disk full") should return "[ERROR] disk full". An empty message still gets the severity prefix and its trailing space.

        In the next exercise, you'll keep one default and replace the other.

        Useful from the Standard Library

        Exercise 4 of 8
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      
          Stuck? Show a hint No spoilers, just a nudge

          Implement only the required method for PlainLogger. The trait's other methods already have bodies and will call your implementation of log. Return an owned copy of the message without adding a prefix.

          Reveal the full solution Spoiler: the complete answer
          /// One required method, with two defaults that build on it.
          trait Logger {
              /// Turn a message into the final log line.
              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}"))
              }
          }
          
          /// A logger that returns the message untouched.
          struct PlainLogger;
          
          impl Logger for PlainLogger {
              fn log(&self, msg: &str) -> String {
                  msg.to_string()
              }
          }
          
          #[test]
          fn log_returns_the_message_untouched() {
              let logger = PlainLogger;
              assert_eq!(Logger::log(&logger, "ready"), "ready");
              assert_eq!(Logger::log(&logger, "  still here  "), "  still here  ");
              assert_eq!(Logger::log(&logger, ""), "");
          }
          
          #[test]
          fn warn_uses_the_default_prefix() {
              let logger = PlainLogger;
              assert_eq!(Logger::warn(&logger, "slow query"), "[WARN] slow query");
              assert_eq!(Logger::warn(&logger, ""), "[WARN] ");
          }
          
          #[test]
          fn error_uses_the_default_prefix() {
              let logger = PlainLogger;
              assert_eq!(Logger::error(&logger, "disk full"), "[ERROR] disk full");
              assert_eq!(Logger::error(&logger, ""), "[ERROR] ");
          }
          

          Overriding Default Methods

          PlainLogger used both default methods without changing them. Now you have a logger that adds a tag, such as "auth: ", to every line. Its log method is already implemented, so the defaults work here too: error("nope") currently returns "auth: [ERROR] nope".

          Make Errors Stand Out

          Add an error method inside the existing impl Logger for TaggedLogger block. Use [CRITICAL] instead of [ERROR], so the same call returns "auth: [CRITICAL] nope". Build the severity-prefixed message and pass it through the logger's own log method so the tag appears exactly once. The tag and message can be any strings, including empty ones.

          Leave the trait and the supplied log method unchanged, and don't add a warn method. Warnings should still use the default body, producing lines such as "auth: [WARN] slow". You only have to override the behavior you want to change.

          Predict Which Methods Run

          Before running the tests, trace Logger::warn(&logger, "slow") and Logger::error(&logger, "nope") for a TaggedLogger with your override. Which calls enter a default body, and which enter your implementation? When a default body calls self.log, whose log runs?

          Does spelling the call as Logger::error force the trait's default body, or still use the override? Check your predictions against the tests and method bodies. The small edit matters because callers use the trait interface in both cases.

          Useful from the Standard Library

          • format! builds a String that you can pass to another method as a string slice.
          Exercise 5 of 8
          Open in Web Editor

          Results

            Compiler / runtime output
            
                        
            Stuck? Show a hint No spoilers, just a nudge

            An override must match the trait's method signature. Tag formatting already belongs to log; keep that responsibility in one place. To trace the calls, look at which methods the implementation supplies and which it inherits.

            Reveal the full solution Spoiler: the complete answer
            /// One required method, with two defaults that build on it.
            trait Logger {
                /// Turn a message into the final log line.
                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}"))
                }
            }
            
            struct TaggedLogger {
                tag: String,
            }
            
            impl Logger for TaggedLogger {
                fn log(&self, msg: &str) -> String {
                    format!("{}: {msg}", self.tag)
                }
            
                fn error(&self, msg: &str) -> String {
                    self.log(&format!("[CRITICAL] {msg}"))
                }
            }
            
            #[test]
            fn error_uses_critical_with_one_tag() {
                let logger = TaggedLogger {
                    tag: "auth".to_string(),
                };
                assert_eq!(Logger::error(&logger, "nope"), "auth: [CRITICAL] nope");
            }
            
            #[test]
            fn error_handles_other_tags_and_messages() {
                for (tag, msg) in [
                    ("worker/7", "queue full"),
                    ("db: replica", "  retry  "),
                    ("", ""),
                ] {
                    let logger = TaggedLogger {
                        tag: tag.to_string(),
                    };
                    assert_eq!(
                        Logger::error(&logger, msg),
                        format!("{tag}: [CRITICAL] {msg}")
                    );
                }
            }
            
            #[test]
            fn overriding_error_leaves_warn_default() {
                let logger = TaggedLogger {
                    tag: "cache".to_string(),
                };
                assert_eq!(Logger::error(&logger, "miss"), "cache: [CRITICAL] miss");
                assert_eq!(Logger::warn(&logger, "miss"), "cache: [WARN] miss");
                assert_eq!(Logger::warn(&logger, ""), "cache: [WARN] ");
            }
            

            Validation Rules

            Two structs can share a validation contract without sharing a base class. The Validator trait requires a check method that borrows both the rule and the input. It returns Ok(()) when the input passes or Err(String) with a failure message.

            Write a Validator implementation for each supplied struct at its comment marker. There is no worked implementation to copy in this step.

            Replace the angle-bracket placeholders with the configured text, keeping the single quotes. For example, a required @ that is missing produces must contain '@'. Match substrings, not just whole inputs, and keep the comparison case-sensitive. Use the usual string containment behavior for empty patterns too: every string, including the empty string, contains the empty pattern.

            The tests will report missing trait implementations until you add them. That is expected; the starter has no incomplete Rust items outside the tests. Keep the trait signature unchanged so either rule can be used wherever a Validator is required.

            Exercise 6 of 8
            Open in Web Editor

            Results

              Compiler / runtime output
              
                          
              Stuck? Show a hint No spoilers, just a nudge

              Match the trait's method signature in both implementation blocks. str::contains checks an entire substring, not whether any one character appears. The two rules have opposite success conditions. Use the configured field when constructing the failure message.

              Reveal the full solution Spoiler: the complete answer
              /// A rule returns `Ok(())` on success or `Err(message)` on failure.
              trait Validator {
                  fn check(&self, input: &str) -> Result<(), String>;
              }
              
              /// Require a case-sensitive substring. On failure, return
              /// `must contain '<needle>'` with the configured 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))
                      }
                  }
              }
              
              /// Reject a case-sensitive substring. On failure, return
              /// `must not contain '<forbidden>'` with the configured text.
              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(())
                      }
                  }
              }
              
              #[cfg(test)]
              mod tests {
                  use super::*;
              
                  #[test]
                  fn required_substring() {
                      let rule = MustContain {
                          needle: "cat".to_string(),
                      };
                      assert_eq!(Validator::check(&rule, "a cat naps"), Ok(()));
                      assert_eq!(Validator::check(&rule, "cat"), Ok(()));
                      for input in ["dog", "CAT", ""] {
                          assert_eq!(
                              Validator::check(&rule, input),
                              Err("must contain 'cat'".to_string())
                          );
                      }
                  }
              
                  #[test]
                  fn forbidden_substring() {
                      let rule = MustNotContain {
                          forbidden: "cat".to_string(),
                      };
                      for input in ["a cat naps", "cat"] {
                          assert_eq!(
                              Validator::check(&rule, input),
                              Err("must not contain 'cat'".to_string())
                          );
                      }
                      for input in ["dog", "CAT", ""] {
                          assert_eq!(Validator::check(&rule, input), Ok(()));
                      }
                  }
              
                  #[test]
                  fn empty_patterns_follow_contains() {
                      let required = MustContain {
                          needle: String::new(),
                      };
                      let forbidden = MustNotContain {
                          forbidden: String::new(),
                      };
                      for input in ["", "anything"] {
                          assert_eq!(Validator::check(&required, input), Ok(()));
                          assert_eq!(
                              Validator::check(&forbidden, input),
                              Err("must not contain ''".to_string())
                          );
                      }
                  }
              }
              

              Borrowing a Mixed Shelf

              The generic description function works for many types, but each call still chooses one concrete element type. A slice of books cannot also hold a movie. Now we want both on the same shelf, without taking ownership of either.

              A borrowed trait object, &dyn Describable, refers to a value through its shared interface. It carries access to both the value and the methods for its concrete type. Different implementors can therefore appear in one slice of trait-object references. This uses dynamic dispatch rather than selecting a concrete implementation through a generic type parameter.

              The book and movie implementations are supplied in this editor. Implement print_mixed_descriptions to return their descriptions in slice order, separated by newlines. An empty shelf returns an empty string. Borrow the existing values; don't clone them or move them into boxes. The function must work with other implementations of Describable too.

              Compare its supplied parameter type with the generic signature you wrote earlier. Why does putting each trait object behind a reference make a mixed slice possible? The owning form, Box<dyn Trait>, is a separate topic in the optional Smart Pointers chapter.

              Exercise 7 of 8
              Open in Web Editor

              Results

                Compiler / runtime output
                
                            
                Stuck? Show a hint No spoilers, just a nudge

                The signature already says how the elements are borrowed. Calling describe through each reference chooses that value's implementation. The string assembly can use the same approach as the generic function.

                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;
                }
                
                struct Book {
                    title: String,
                    author: String,
                }
                
                struct Movie {
                    title: String,
                    year: u16,
                }
                
                impl Describable for Book {
                    fn describe(&self) -> String {
                        format!("{} by {}", self.title, self.author)
                    }
                }
                
                impl Describable for Movie {
                    fn describe(&self) -> String {
                        format!("{} ({})", self.title, self.year)
                    }
                }
                
                // One generic T cannot be both Book and Movie. An enum would work for a closed
                // set of types; trait objects also accept future implementors. These references
                // borrow the originals, so no Box or clone is needed.
                /// Describe a mixed shelf in order, separated by newlines. Empty input returns
                /// "".
                fn print_mixed_descriptions(items: &[&dyn Describable]) -> String {
                    let mut lines = Vec::new();
                    for item in items {
                        lines.push(item.describe());
                    }
                    lines.join("\n")
                }
                
                #[test]
                fn mixed_shelf_borrows_book_and_movie() {
                    let book = Book {
                        title: "Dune".into(),
                        author: "Herbert".into(),
                    };
                    let movie = Movie {
                        title: "Arrival".into(),
                        year: 2016,
                    };
                    let shelf: Vec<&dyn Describable> = vec![&book, &movie];
                    assert_eq!(
                        print_mixed_descriptions(&shelf),
                        "Dune by Herbert\nArrival (2016)"
                    );
                    assert_eq!(
                        print_mixed_descriptions(&[&movie, &book]),
                        "Arrival (2016)\nDune by Herbert"
                    );
                    assert_eq!(book.title, "Dune");
                    assert_eq!(movie.year, 2016);
                }
                
                #[test]
                fn mixed_shelf_can_be_empty() {
                    assert_eq!(print_mixed_descriptions(&[]), "");
                }
                
                #[test]
                fn accepts_other_implementors() {
                    struct Poster;
                    impl Describable for Poster {
                        fn describe(&self) -> String {
                            "Art print".into()
                        }
                    }
                    assert_eq!(print_mixed_descriptions(&[&Poster]), "Art print");
                }
                

                Collect Every Error

                A failed check should not hide the failures that come after it. For "abc", a minimum length of eight and a required @ both fail, and the caller needs both messages.

                The mixed shelf exercise used trait objects to borrow values of different concrete types through one trait. Here, &dyn Validator plays the same role for validation rules. The supplied &[&dyn Validator] parameter borrows a slice of those references, so length and substring rules can appear together. Calling check through a trait object selects that rule's implementation at runtime. The collector does not need to know the concrete rule type.

                Implement only the body of collect_errors. The rule implementations are supplied so this exercise stands on its own.

                A passing rule between two failing rules must not interrupt collection or add a message. How does this behavior differ from propagating the first error with ??

                Exercise 8 of 8
                Open in Web Editor

                Results

                  Compiler / runtime output
                  
                              
                  Stuck? Show a hint No spoilers, just a nudge

                  Inspect each rule's result and keep only its error message. A failure should not stop the loop: the caller needs every message in rule order. Using ? here would express the wrong behavior.

                  Reveal the full solution Spoiler: the complete answer
                  trait Validator {
                      fn check(&self, input: &str) -> Result<(), String>;
                  }
                  
                  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(())
                          }
                      }
                  }
                  
                  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))
                          }
                      }
                  }
                  
                  /// Run every rule. Return failure messages in rule order, or an empty vector.
                  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
                  }
                  
                  #[cfg(test)]
                  mod tests {
                      use super::*;
                  
                      #[test]
                      fn failures_stay_in_order_with_a_pass_between_them() {
                          let length = MinLength { n: 8 };
                          let passing = MustContain { needle: "b".into() };
                          let missing = MustContain { needle: "@".into() };
                          let rules: [&dyn Validator; 3] = [&length, &passing, &missing];
                          assert_eq!(
                              collect_errors(&rules, "abc"),
                              vec!["must be at least 8 characters", "must contain '@'"]
                          );
                      }
                  
                      #[test]
                      fn all_pass_or_no_rules() {
                          let length = MinLength { n: 3 };
                          let required = MustContain { needle: "@".into() };
                          assert!(collect_errors(&[&length, &required], "a@b").is_empty());
                          assert!(collect_errors(&[], "anything").is_empty());
                      }
                  
                      struct RejectInput;
                  
                      impl Validator for RejectInput {
                          fn check(&self, input: &str) -> Result<(), String> {
                              Err(format!("rejected '{input}'"))
                          }
                      }
                  
                      #[test]
                      fn custom_rules_receive_the_input_and_keep_duplicate_messages() {
                          let missing = MustContain { needle: "@".into() };
                          let rules: [&dyn Validator; 3] = [&missing, &RejectInput, &RejectInput];
                          assert_eq!(
                              collect_errors(&rules, "abc"),
                              vec!["must contain '@'", "rejected 'abc'", "rejected 'abc'"]
                          );
                      }
                  }
                  

                  Wrapping Up Traits

                  You gave existing types a shared interface, then wrote callers that depend on that interface rather than on concrete types. Separating those jobs let you test each implementation before combining values in a collection.

                  What We Learned

                  • A trait declares behavior; an implementation connects that behavior to a particular type. An inherent method with the same name does not make the type implement the trait.
                  • Display lets standard formatting code call your implementation. The standard library does not derive it because it cannot choose a human-readable representation for you.
                  • A generic trait bound lets one function work with different types across calls, while each call uses a single concrete type.
                  • A default method can call a required method supplied by the implementor. Override only the behavior that needs to differ.
                  • Borrowed trait objects let one collection refer to different concrete types through a common interface. They do not take ownership of the values.
                  • A caller decides how to handle each method's result. The validation collector keeps checking after errors so it can report every failure.

                  The optional Smart Pointers chapter takes the next ownership step: returning a collection that owns different command types rather than borrowing them.

                  Next chapter 18Iterators

                  Optional Chapters

                  Extra practice to explore at your own pace. These chapters do not count toward course progress.

                  ← Return to the Rust course curriculum