Build the mental model
A trait defines a set of method signatures a type can implement, similar in purpose to a Java interface, a Go interface, or a TypeScript interface — you reach for a trait when multiple types share and implement the same behavior. Connecting Lesson 9's struct plus `impl` block to a trait (`impl Summarize for LessonStat { ... }`) makes the compiler enforce that a type actually fulfills the trait's contract — missing a trait method is a compile error. A trait bound (`fn print_summary<T: Summarize>(item: &T)`) is a direct extension of Lesson 13's constraint idea, restricting a generic function to only what operations it can actually call — `T: Summarize` is a promise to the compiler that `T` implements every method the `Summarize` trait requires. The `impl Trait` syntax (`fn make_lesson() -> impl Summarize`) hides the concrete return type from the caller, offering an abstraction of "this returns something implementing this trait" — similar to returning a Java interface type, but in Rust it uses static dispatch (resolved at compile time), which is faster than dynamic dispatch (`dyn Trait`, resolved at runtime). A key difference from Java or C++ interfaces and abstract classes is that Rust traits can be implemented for a type after the fact, even from external code, which favors composition-based design over inheritance-based design.
Connect it to a real scenario
To share common summary behavior across the content analyzer's `LessonStat` and `Tag` structs, define `trait Summarize { fn summary(&self) -> String; }` and write `impl Summarize for LessonStat` and `impl Summarize for Tag`, so report-generation code becomes type-agnostic: `fn print_report<T: Summarize>(item: &T) { println!("{}", item.summary()); }`. To hold a mixed vector of both `LessonStat` and `Tag` items together, you need `Vec<Box<dyn Summarize>>` (dynamic dispatch, connecting to Lesson 17's `Box<T>`) — static dispatch (`impl Trait`/generics) only applies when the compiler needs to know one single type upfront.
Try the working example
trait Summarize {
fn summary(&self) -> String;
}
struct LessonStat { title: String, word_count: u32 }
struct Tag { name: String, uses: u32 }
impl Summarize for LessonStat {
fn summary(&self) -> String {
format!("{}: {} words", self.title, self.word_count)
}
}
impl Summarize for Tag {
fn summary(&self) -> String {
format!("#{} ({}x)", self.name, self.uses)
}
}
fn print_summary<T: Summarize>(item: &T) {
println!("{}", item.summary());
}
fn main() {
print_summary(&LessonStat { title: String::from("Traits"), word_count: 700 });
print_summary(&Tag { name: String::from("rust"), uses: 12 });
}You print "Traits: 700 words" and "#rust (12x)" from the same one function.5-minute try-it
Create `trait Describable { fn describe(&self) -> String; }` — implement it for two structs of your choosing — call both through one generic function `fn show<T: Describable>(item: &T)`.
One important caution
Defining a `trait` but forgetting to write `impl Trait for Type` for a given type — calling a trait method then hits a "method not found" compile error; defining a trait alone doesn't grant its methods.
Trying to hold a mixed-type collection (a list containing different types) in a static-dispatch generic (`Vec<T>`) — overlooking that generics need one single type known at compile time; `Vec<Box<dyn Trait>>` is needed instead.