Step 3 of 7
Supertraits and fully qualified syntax
Supertraits: "to implement this, you must also implement that"
Sometimes a trait only makes sense for types that already have another ability. An alert must be printable, so Alert can require Display:
use std::fmt;
trait Alert: fmt::Display {
fn level(&self) -> u8;
fn banner(&self) -> String {
let tag = if self.level() >= 3 { "CRITICAL" } else { "notice" };
format!("[{tag}] {self}")
}
}
struct DiskFull {
mount: String,
percent: u8,
}
impl fmt::Display for DiskFull {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is {}% full", self.mount, self.percent)
}
}
impl Alert for DiskFull {
fn level(&self) -> u8 {
if self.percent >= 95 { 3 } else { 1 }
}
}
fn main() {
let var = DiskFull { mount: "/var".to_string(), percent: 97 };
let home = DiskFull { mount: "/home".to_string(), percent: 80 };
println!("{}", var.banner());
println!("{}", home.banner());
}
[CRITICAL] /var is 97% full
[notice] /home is 80% full
trait Alert: fmt::Display makes Display a supertrait of Alert. Two things follow: every type that implements Alert must also implement Display, and code inside Alert (like the default banner) may use {self} because it's guaranteed to work. A function taking T: Alert can print T too, without also writing T: Display.
It looks like inheritance, but it isn't: no fields or method bodies are inherited. It's a requirement, checked by the compiler. std is full of them: Copy: Clone, Eq: PartialEq, Ord: Eq + PartialOrd, and std::error::Error: Debug + Display (that's why your error types in the custom-errors module needed both).
use std::fmt::Display;
trait Loggable: Display {
fn log_line(&self) -> String {
format!("[LOG] {self}")
}
}
struct Ticket {
id: u32,
}
impl Loggable for Ticket {}
fn main() {
let t = Ticket { id: 1 };
println!("{}", t.id);
}
error[E0277]: `Ticket` doesn't implement `std::fmt::Display`
Two traits, one method name
Nothing stops two traits from having methods with the same name. When one type implements both, a plain method call is ambiguous, so you name the trait. This is fully qualified syntax.
trait Employee {
fn id(&self) -> String;
fn role() -> &'static str;
}
trait Customer {
fn id(&self) -> String;
fn role() -> &'static str;
}
struct Person {
staff_no: u32,
account: u32,
}
impl Employee for Person {
fn id(&self) -> String {
format!("E-{}", self.staff_no)
}
fn role() -> &'static str {
"employee"
}
}
impl Customer for Person {
fn id(&self) -> String {
format!("C-{}", self.account)
}
fn role() -> &'static str {
"customer"
}
}
fn main() {
let ana = Person { staff_no: 7, account: 1042 };
println!("{}", Employee::id(&ana));
println!("{}", Customer::id(&ana));
println!("{}", <Person as Employee>::role());
println!("{}", <Person as Customer>::role());
}
E-7
C-1042
employee
customer
- Methods with
self:Employee::id(&ana)calls the trait's method as a plain function, passing the receiver yourself. Rust knows the type from&ana. - Associated functions without
self:role()has no receiver, soEmployee::role()can't know which type you mean. The full form<Person as Employee>::role()says "theEmployeeimpl ofPerson". You'll also meet it in generic code, like<T as Iterator>::Item. - If
Personhad its own (inherent)idmethod,ana.id()would call that one; inherent methods win over trait methods.
Common mistakes
- Calling
ana.id()with two traits in scope:error[E0034]: multiple applicable items in scope. The message lists both candidates and suggests the fully qualified forms. Employee::role()with no type:
trait Named {
fn name() -> String;
}
struct Crab;
impl Named for Crab {
fn name() -> String {
"Ferris".to_string()
}
}
fn main() {
println!("{}", Named::name());
}
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
Your task
Distance implements both Metric and Imperial, which have the same two names: unit() (no self) and value(&self). Write describe, which returns both measurements with one decimal place, like 12.0 m = 39.4 ft. Get the units from the traits with fully qualified syntax rather than typing "m" and "ft".
Challenge 2
Ferris is both a pilot and a chef. Fill in the blanks to call each trait's version, so the program prints:
Captain Ferris on the bridge
Chef Ferris in the kitchen
pilot and chef
Challenge 3
Write a trait with two supertraits. Sellable must require both Priced (given) and fmt::Display, and have a default method fn receipt_line(&self) -> String that returns the item's display text, a colon, and its price in dollars, like Rust in Action: $39.99.
Then make Book sellable: implement Display (shows the title), Priced (returns price_cents), and an empty impl Sellable for Book {}. Finally write fn total_cents<T: Sellable>(items: &[T]) -> u32, which only needs the Sellable bound to add up the prices.
Previous: Operator traits and default type parameters Next: Blanket impls and extension traits