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.
impl Display for Temperature so a value formats itself as "21.5°C" in println! and format!.Describable trait with two implementations, plus a generic function with a trait bound that accepts any T: Describable.dyn Trait). Generics give you one specialized copy of a function per concrete type ("static dispatch").
Trait objects give you one function that dispatches at runtime ("dynamic dispatch") and let you put different types into the same Vec.| Trait | What it gives you | First seen |
|---|---|---|
Debug | {:?} formatting | the enums chapter |
Display | {} formatting | this chapter |
PartialEq, Eq | == and != | the enums chapter |
Clone, Copy | .clone() and implicit copies | the structs and methods chapter |
Default | T::default() | mentioned in passing |
Iterator | for x in iter, all the combinators | the iterators chapter |
From, Into | T::from(x) and x.into() conversions | sprinkled throughout |
None of those are magic.
Each is a regular trait defined in std, and the standard library impls it for the obvious built-in types.
When you derive one, the compiler writes the impl.
When you can't derive (maybe the auto-generated version isn't what you want), you write impl Display for MyType { ... } by hand, just like in step 2.
// Static dispatch: the compiler generates a specialized copy of
// `print_all` for each `T` you use it with. Zero runtime cost,
// but every `T` in one call must be the same concrete type.
fn print_all<T: Display>(items: &[T]) { /* ... */ }
// Dynamic dispatch: one function, one vtable lookup per call.
// The slice can mix different concrete types that all implement
// `Display`.
fn print_all_dyn(items: &[&dyn Display]) { /* ... */ }
You'll write both in this chapter.
The Box<dyn Trait> form, which solves the "but trait objects don't have a known size" problem you'll bump into, is the headline act of the smart pointers chapter.
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".
#[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.- Format specifiers carry over:
{:.1}rounds a float to one decimal place, soformat!("{:.1}", 21.5_f64)is"21.5". You'll want that for the temperature output.
fmt body is one write! call.write! takes the formatter, then a format string, then the args: write!(f, "{:.1}°C", self.celsius).write! already returns fmt::Result, so its result is your return value.
No semicolon on the last line, or use an explicit return.
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"`
///
/// Hint: `write!(f, "{:.1}°C", self.celsius)` does the whole job.
/// The `:.1` is the same format specifier you'd use in `println!`.
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");
}
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 { ... } }.
Once a trait exists, you can use it as a bound on a generic parameter to say "I accept any T, as long as T implements this trait":
fn print_one<T: Describable>(item: &T) {
println!("{}", item.describe());
}
This is Rust's answer to "polymorphism."
The compiler stamps out one specialized copy of print_one per type you call it with (called monomorphization; the C++ template crowd will feel at home).
There's no runtime dispatch and no boxing.
A few variations you'll meet in real code, just so you recognize them:
// Multiple bounds with `+`:
fn show<T: Describable + std::fmt::Debug>(item: &T) { /* ... */ }
// Same thing, written with a `where` clause. Easier to read once you
// have several parameters or long bounds:
fn show<T>(item: &T)
where
T: Describable + std::fmt::Debug,
{
/* ... */
}
// `impl Trait` in argument position is shorthand for a single
// unnamed generic parameter:
fn show(item: &impl Describable) { /* ... */ }
You'll write the simple <T: Describable> form in this step.
The others are sugar for the same underlying machinery.
Useful from the standard library
- The Rust Book on traits walks through definitions, implementations, and bounds with more examples than fit here.
Vec<String>::join("\n")(and any&[String].join(...)) is handy for theprint_descriptionsexercise: build aVec<String>of per-item descriptions, then join them with newlines.- The standard
Iterator::mapplus.collect::<Vec<_>>()is the idiomatic way to turn a&[T]into aVec<String>. You met both in passing; the iterators chapter covers iterators properly.
describe methods are one-line format! calls:
format!("{} by {}", self.title, self.author) for Book.format!("{} ({})", self.title, self.year) for Movie.print_descriptions, build a Vec<String> and join it:
let lines: Vec<String> = items.iter().map(|x| x.describe()).collect();
lines.join("\n")
.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")
/// 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), "");
}
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.
You'll work with a small Logger trait.
There's exactly one required method (log, which formats a single line) and two default methods (warn and error) that build on top of it.
trait Logger {
fn log(&self, msg: &str) -> String;
fn warn(&self, msg: &str) -> String {
self.log(&format!("[WARN] {msg}"))
}
fn error(&self, msg: &str) -> String {
self.log(&format!("[ERROR] {msg}"))
}
}
This is deliberately recognisable: every logging library in every language has this same skeleton.
The teaching value of the default methods is that they live on the trait, so adding a new implementor doesn't mean writing warn and error from scratch every time.
Two implementors:
PlainLogger returns the message untouched.
It uses both defaults as written, so all you have to write is log.TaggedLogger { tag: String } prepends a tag (e.g. "auth: ...").
It uses the default warn, but overrides error to swap the [ERROR] prefix for a louder [CRITICAL] prefix.That asymmetry is the point: implementors take what they want from the defaults and override only the bits they need to change.
Useful from the standard library
- The
format!macro is the workhorse here. Defaultwarnbuilds"[WARN] {msg}"and hands it back toself.log, so whatever decorationlogdoes (the tag, inTaggedLogger's case) wraps the warning prefix.- Default methods are written inside the
traitblock, with a body instead of a trailing semicolon. The required methods (the ones without a body) are still mandatory; defaults are bonus.
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.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.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}"))
}
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.
/// 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");
}
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.
dyn TraitWhen you want a single collection that holds different concrete types as long as they all implement the same trait, you reach for a trait object, spelled dyn Trait:
fn run_all(items: &[&dyn Validator], input: &str) {
for v in items {
let _ = v.check(input);
}
}
Two things changed compared to the generic version:
&dyn Validator is a fat pointer: two words that together point at the value and at a vtable of function pointers (one per trait method).
At each call to .check(...), Rust looks the function up in the vtable.
C++ folks: this is the same machinery as virtual methods, just opt-in per call site instead of per class.run_all is compiled exactly once.
The price is the indirection through the vtable; the payoff is heterogeneous collections.fn f<T: Trait>(x: &T) | fn f(x: &dyn Trait) | |
|---|---|---|
| Dispatch | static, decided at compile time | dynamic, vtable lookup at runtime |
| Code size | one copy per T you use | one copy total |
| Mixed collections | no | yes |
| Runtime cost | none | one 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.
You'll build a tiny composable validation library. The trait is deliberately small:
trait Validator {
/// `Ok(())` on success, `Err(message)` on failure.
fn check(&self, input: &str) -> Result<(), String>;
}
Three implementors, each enforcing one rule:
MinLength { n }: input must have at least n characters.MustContain { needle }: input must contain the given substring.MustNotContain { forbidden }: input must not contain the given substring.The killer feature is that MinLength, MustContain, and MustNotContain are three different types, but a single &[&dyn Validator] slice can hold all of them at once.
That's what makes "pluggable rules" work: every rule is a different struct (and might carry different configuration), but the call site only knows "a list of things that can validate."
You'll see the same idea, scaled up, in the password validator chapter: a configurable set of checks running against one input.
Box<dyn Trait>You'll see Box<dyn Trait> everywhere in real Rust code:
let rules: Vec<Box<dyn Validator>> = vec![
Box::new(MinLength { n: 8 }),
Box::new(MustContain { needle: "@".to_string() }),
];
The reason: dyn Trait has no statically known size (the three implementors above can carry different fields, so they don't all take up the same number of bytes), so the compiler won't let you put bare dyn Validator values directly in a Vec.
A Box is a heap allocation with a fixed-size pointer that lives on the stack, which sidesteps the size problem.
That's exactly what the smart pointers chapter unpacks.
For this step we sidestep it with &dyn Validator references, which are also fixed-size and work fine for borrowing.
Useful from the standard library
- The Rust Book on trait objects.
str::contains(with a&strargument) is all you need for theMustContain/MustNotContainchecks.- Inside
collect_errors, a plainforloop pushing into aVec<String>is the most direct form. The same chain with.iter().filter_map(...)works once you've met iterators in the iterators chapter.
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(())
}
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
&dyn Validator means each v in the loop is a &&dyn Validator.
Method calls auto-deref, so v.check(input) works without ceremony.validators.iter().filter_map(|v| v.check(input).err()).collect().
/// 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());
}
You implemented an stdlib trait (Display), defined your own (Describable), wrote a generic function with a trait bound, gave a trait a default method that one type inherited and another overrode (the Logger step), and finally swapped a generic for a trait object so a single slice could hold a mix of validation rules.
What we learned
- A trait is a named collection of method signatures. Any type can opt in with
impl TraitName for TypeName { ... }. Same idea as Java/C# interfaces, Haskell type classes, Swift protocols, or C++ abstract classes with pure virtual methods.#[derive(...)]is sugar: the compiler writes the obviousimpl Trait for Typeblock for you.Debug,PartialEq,Eq,Clone,Copy,Default,Hash, andOrdare the everyday derivable ones.Displayis not derivable because there's no one-size-fits-all human-readable format.impl Displayis thetoString/__str__of Rust. Implementfn fmt(&self, f: &mut Formatter<'_>) -> fmt::Resultwith a singlewrite!(...)call.- Trait bounds on a generic say "I accept any
Tthat implements this trait."fn f<T: Trait>(x: &T)is the basic form,T: A + Bcombines bounds, andwhereclauses let you push long bounds out of the signature.- Default methods in the trait body give every implementor a baseline behavior. Override per type when you need to.
- Generics dispatch statically: one specialized copy of the function per concrete
T. Fast, but one call can only see one concrete type. Great default.- Trait objects (
dyn Trait) dispatch dynamically through a vtable. Slower per call but lets one slice orVechold values of multiple concrete types at once. Reach for them when you need heterogeneity.Box<dyn Trait>solves the "trait objects have no known size" problem so they can live in owning containers likeVec. That's the headline of the optional smart pointers bonus chapter (Box,Rc,RefCell). Dip into it whenever you want to go deeper.- Many stdlib traits you already use (
Iterator,From,Into,PartialEq, ...) are just regular traits. Nothing magic. You can define your own or implement the standard ones for your own types.