Step 3 of 6
Traits as parameters: bounds and impl Trait
The real payoff of a trait: functions that accept any type with that behavior. You already know the syntax from generics, where you wrote bounds like T: PartialOrd. A trait bound works the same with your own traits.
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
}
struct Post {
user: String,
text: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("Article: {}", self.title)
}
}
impl Summary for Post {
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.text)
}
}
fn preview(item: &impl Summary) {
println!("Preview -> {}", item.summarize());
}
fn feed<T: Summary>(items: &[T]) {
for item in items {
println!("- {}", item.summarize());
}
}
fn main() {
let a = Article { title: String::from("Crabs can't jump") };
let p = Post { user: String::from("ana"), text: String::from("first!") };
preview(&a);
preview(&p);
let posts = vec![
Post { user: String::from("bo"), text: String::from("gg") },
Post { user: String::from("cy"), text: String::from("wp") },
];
feed(&posts);
}
Preview -> Article: Crabs can't jump
Preview -> @ana: first!
- @bo: gg
- @cy: wp
item: &impl Summaryreads "a reference to some type that implementsSummary". It's called impl Trait in argument position, and it's short for the generic versionfn preview<T: Summary>(item: &T).fn feed<T: Summary>(items: &[T])is the longer form: a type parameterTwith a bound. (items: &[impl Summary]would work here too.) You need the named form when the same type must appear in more than one place, as the next section shows.- Inside the function you may only use what the bound promises.
item.summarize()is fine;item.titleis not, because not everySummarytype has a title.
Like all generics, this is monomorphized: the compiler writes a separate copy of preview for Article and one for Post. Calling through a trait bound is exactly as fast as calling the method directly.
One type, or many?
These two signatures are not the same:
fn both(a: &impl Summary, b: &impl Summary) // a and b may be different types
fn both<T: Summary>(a: &T, b: &T) // a and b must be the SAME type T
And feed(&[T]) takes a slice where every item is the same type T: a Vec can't mix Articles and Posts. Mixing types in one collection needs trait objects, a later module.
Common mistakes
- Forgetting the bound. Without it,
Tcould be any type at all, so the compiler won't let you callsummarize. Read the help line: it tells you the fix.
trait Summary {
fn summarize(&self) -> String;
}
fn feed<T>(items: &[T]) {
for item in items {
println!("{}", item.summarize());
}
}
fn main() {}
error[E0599]: no method named `summarize` found for reference `&T` in the current scope
= help: items from traits can only be used if the type parameter is bounded by the trait
help: the following trait defines an item `summarize`, perhaps you need to restrict type parameter `T` with it:
|
5 | fn feed<T: Summary>(items: &[T]) {
- Using a field through the bound:
item.titleon an&impl Summarygiveserror[E0609]: no field `title` on type `&impl Summary`. Add a method to the trait if every type should provide it.
Your task
A café's menu items implement Priced. Write two functions:
fn total_cents<T: Priced>(items: &[T]) -> u32: the sum of the prices (0 for an empty slice).fn can_afford(item: &impl Priced, budget_cents: u32) -> bool: true if the item's price is at most the budget.
Challenge 2
Write fn cheaper(a: &impl Priced, b: &impl Priced) -> u32 that returns the lower of the two prices. Because each parameter has its own impl Priced, you can compare a coffee with a bagel.
Challenge 3
Write fn digest<T: Summary>(items: &[T]) -> String that numbers the summaries, one per line, like this for two posts:
1. @bo: gg
2. @cy: wp
Lines are separated by \n with no newline at the end. An empty slice gives an empty string.