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.
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.
| Trait | What it gives you | Where you know it from |
|---|---|---|
Debug | {:?} formatting | enums |
Display | {} formatting | the exercises below |
PartialEq, Eq | == and != | enums |
Clone, Copy | .clone() and implicit copies | structs and methods |
Default | T::default() | earlier mentions |
Iterator | for x in iter, all the combinators | iterator pipelines in later exercises |
From, Into | T::from(x) and x.into() conversions | earlier 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.
Throughout the exercises, you'll encounter two ways to use traits, called static and dynamic dispatch. Here's what each looks like:
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]) { /* ... */ }
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]) { /* ... */ }
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.
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".
#[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::Displayis the trait.use std::fmt;and thenimpl fmt::Display for Tis the idiomatic spelling.write!is the formatter-targeted cousin ofprintln!. It returnsstd::fmt::Result, which is exactly what yourfmtmethod needs to return, so a singlewrite!(...)call is usually the whole body.- The formatting syntax reference explains how to set floating-point precision.
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.
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");
}
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.
Book returns "<title> by <author>", for example "Dune by Herbert".Movie returns "<title> (<year>)", for example "Arrival (2016)".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.
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.
/// 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");
}
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::pushcan collect owned descriptions from a loop.slice::joincombines string elements with a separator, including empty elements.
You don't need iterator adapters for this exercise; those come in the next chapter.
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.
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");
}
}
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.
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
str::to_stringcreates an ownedStringfrom a string slice.
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.
/// 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] ");
}
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".
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.
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 aStringthat you can pass to another method as a string slice.
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.
/// 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] ");
}
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.
MustContain succeeds when the input contains needle and otherwise returns
must contain '<needle>'.MustNotContain succeeds when the input does not contain forbidden and
otherwise returns must not contain '<forbidden>'.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.
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.
/// 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())
);
}
}
}
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.
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.
/// 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");
}
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.
Validator, not just the two supplied types.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
??
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.
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'"]
);
}
}
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.
Displaylets 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.
Extra practice to explore at your own pace. These chapters do not count toward course progress.