Step 1 of 7 · Reading · ~4 min
Learn
Traits and Generics
Traits and Generics
By the end of this lesson you can write one function or struct that works for any type offering the behaviour it needs, name that behaviour with a trait, and make your own types printable with {}.
In Rust Fundamentals, impl blocks gave one type its methods. A trait lets many types promise the same methods; a generic is code written once against that promise.
A trait is a list of promises
trait Priced {
fn cents(&self) -> i64; // required: no body, every type must write it
fn tag(&self) -> String { // default: has a body, a type may keep or replace it
format!("{}.{:02}", self.cents() / 100, self.cents() % 100)
}
}
struct Book { pages: i64 }
struct Coffee { ml: i64 }
impl Priced for Book {
fn cents(&self) -> i64 { self.pages * 3 }
}
impl Priced for Coffee {
fn cents(&self) -> i64 { 250 + self.ml / 10 }
fn tag(&self) -> String { format!("coffee {}", self.cents()) } // override
}
impl Priced for i64 { // an existing type works too
fn cents(&self) -> i64 { *self }
}
Book { pages: 150 }.tag() is 4.50 (the default, from cents() = 450), Coffee { ml: 300 }.tag() is coffee 280, and 995_i64.tag() is 9.95.
You may implement your own trait for any type, and any trait for your own type, but not a foreign trait for a foreign type: impl fmt::Display for Vec<i64> is rejected as error[E0117] (the orphan rule).
Generics and bounds
fn total<T: Priced>(items: &[T]) -> i64 means: for any type T that implements Priced. Priced is the bound. The body is checked once for every possible T, so it may use only what the bound promises:
fn total<T>(items: &[T]) -> i64 { // no bound
let mut sum = 0;
for it in items { sum += it.cents(); }
sum
}
// error[E0599]: no method named `cents` found for type `&T` in the current scope
With T: Priced the same body compiles, and total(&[Book { pages: 150 }, Book { pages: 10 }]) is 450 + 30 = 480. A type with no impl is refused at the call site:
total(&[Mug]) // error[E0277]: the trait bound `Mug: Priced` is not satisfied
Several bounds join with + (T: Priced + fmt::Display), and a where clause moves them below a long signature:
fn describe<T>(x: &T) -> String where T: Priced + fmt::Display { format!("{} {}", x, x.tag()) }
In an argument, x: &impl Priced is shorthand for <T: Priced>(x: &T).
Generic structs
Put the bound on the impl, where the methods that need it live:
struct Basket<T> { lines: Vec<T> }
impl<T: Priced> Basket<T> {
fn new() -> Basket<T> { Basket { lines: Vec::new() } }
fn put(&mut self, line: T) { self.lines.push(line); }
fn sum(&self) -> i64 { total(&self.lines) }
}
Once the first put(Book { .. }) runs, this basket is a Basket<Book>, and a later put(Coffee { ml: 300 }) fails with error[E0308]: mismatched types (expected Book, found Coffee). One value holds one concrete type; mixing types needs dyn Trait, covered in Rust Advanced.
The compiler builds one copy of total per concrete type used (total::<Book>, total::<i64>), and each call goes straight to its copy. This is static dispatch.
Display and derive
{} works for a type that implements fmt::Display, which you write by hand because only you know how the type should read:
use std::fmt;
impl fmt::Display for Book {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "book({}p)", self.pages)
}
}
// println!("{}", Book { pages: 150 }) prints book(150p)
Mechanical traits are written by #[derive(...)]: Clone, Copy (every field must be Copy), PartialEq, PartialOrd (fields in declaration order, so Pt { x: 1, y: 9 } < Pt { x: 2, y: 0 }) and Default (Pt::default() is Pt { x: 0, y: 0 }).
Common mistakes
- Letting a tie go to the last item. Replacing the best with
>=makes the inputba ab(both score 3) reportabinstead ofba. The first of equal scores must stay. - Scoring by raw character code.
'H'is 72 and'!'is 33, so using the code directly, or subtractingawithout lowercasing, gives nonsense or an overflow panic. Only ASCII letters count, soHi!is 8 + 9 = 17. - Scoring a negative number as itself. For
-9 3the winner is-9, because its score is 9. - Putting the bound nowhere. A generic
impl<T>that callsscore()on aTis the E0599 above.
Your exercise
The starter reads the input, prints the report and compiles, but each TODO is a placeholder. Write the scoring, the shared default, the displays and the winner search. The tie rule is the likely slip: only a strictly greater score replaces the best so far.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…