Traits

Rust · Practical · 14 min

Traits are Rust's mechanism for polymorphism and code reuse; the whole standard library is built on them.

A trait declares a set of methods that types can implement, similar to an interface. You write trait Summary { fn summarize(&self) -> String; } and then impl Summary for Article { ... } for each type.

Traits can provide default methods that implementors get for free and may override. A default method can call the required ones, so a trait can ask for one small method and offer many on top. This is exactly how Iterator works: implement next, and you get map, filter, sum and dozens more.

To accept "anything that implements Summary", the simplest syntax is fn notify(item: &impl Summary). In return position, fn make() -> impl Summary returns some concrete type the caller cannot name. Both are resolved at compile time, with no runtime cost.

The orphan rule says you can implement a trait for a type only if the trait or the type is defined in your crate. You can implement your trait for Vec<T>, or Display for your struct, but not Display for Vec<T>. The usual workaround is a newtype wrapper.

Traits can require other traits (supertraits): trait Pretty: std::fmt::Display { ... } means implementors must also implement Display, and default methods may use to_string().

To call a trait method, the trait must be in scope. If x.summarize() mysteriously fails with "method not found", you probably forgot use crate::Summary;. Many crates provide a prelude module for this reason.

Since Rust 1.75, traits can contain async fn and -> impl Trait methods too.

Example

use std::fmt::Display;

trait Summary {
    fn author(&self) -> String;
    fn summarize(&self) -> String { format!("(Read more from {}...)", self.author()) } // default
}

struct Tweet { user: String, text: String }
struct Article { by: String }
impl Summary for Tweet {
    fn author(&self) -> String { format!("@{}", self.user) }
    fn summarize(&self) -> String { format!("{}: {}", self.author(), self.text) } // override
}
impl Summary for Article {
    fn author(&self) -> String { self.by.clone() }
}

fn notify(item: &impl Summary) { println!("Breaking! {}", item.summarize()); }
fn loud<T: Display>(x: T) -> impl Display { format!("{x}!") }

fn main() {
    notify(&Tweet { user: "rustlang".into(), text: "1.88 is out".into() }); // Breaking! @rustlang: 1.88 is out
    notify(&Article { by: "Ferris".into() }); // Breaking! (Read more from Ferris...)
    println!("{}", loud(42));                 // 42!
}

Remember

  • A trait lists required methods and may supply default ones
  • &impl Trait parameters and -> impl Trait returns are static and zero-cost
  • Orphan rule: you own the trait or the type; bring traits into scope to call their methods

Watch out for

  • Forgetting to use a trait and wondering why its method does not exist.

Check yourself

  1. Which impl is forbidden by the orphan rule in your own crate?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/book/ch10-02-traits.html