Guide contents

Lesson 29 of 94

Traits: Rust's Interfaces

A trait defines behavior that types can share. It is central to the language: nearly every abstraction, including printing, comparing, iterating, and operators, is a trait.

Key points about traits:

  • A type implements a trait in a separate impl Trait for Type block, not in the type's declaration. You can even implement your own traits for types you didn't write, such as i32 or String.
  • There is no inheritance of implementation between types. Traits can have default methods, and that is the main way code is shared.
  • Traits power both static dispatch (generics, resolved at compile time, zero cost) and dynamic dispatch (dyn Trait, resolved at runtime through virtual calls). This lesson and the next ones cover both.
Concept Rust
Declaring a trait trait Shape { fn area(&self) -> f64; }
Implementing a trait impl Shape for Circle { ... }
Default methods Default method bodies in the trait
Static methods Associated functions in the trait
Taking any implementor fn draw(s: &impl Shape) (generic) or fn draw(s: &dyn Shape) (dynamic)
Adding a trait to an existing type You can implement your own trait for String

1. Defining and implementing a trait

Rust
trait Describe {
    fn describe(&self) -> String;
}

struct Book {
    title: String,
    author: String,
}

struct Movie {
    title: String,
    minutes: u32,
}

impl Describe for Book {
    fn describe(&self) -> String {
        format!("'{}' by {}", self.title, self.author)
    }
}

impl Describe for Movie {
    fn describe(&self) -> String {
        format!("'{}' ({} min)", self.title, self.minutes)
    }
}

fn main() {
    let b = Book { title: String::from("Dune"), author: String::from("Frank Herbert") };
    let m = Movie { title: String::from("Arrival"), minutes: 116 };
    println!("{}", b.describe());
    println!("{}", m.describe());
}
Output
'Dune' by Frank Herbert
'Arrival' (116 min)

2. Every method must be implemented

Leaving out a required method is an error.

Rust✗ does not compile · E0046
trait Shape {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
}

struct Square {
    side: f64,
}

impl Shape for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn main() {}
Output
error[E0046]: not all trait items implemented, missing: `perimeter`
  --> src/main.rs:10:1
   |
 3 |     fn perimeter(&self) -> f64;
   |     --------------------------- `perimeter` from trait
...
10 | impl Shape for Square {
   | ^^^^^^^^^^^^^^^^^^^^^ missing `perimeter` in implementation

3. Default methods

A trait can provide a method body. Implementors get it for free and may override it.

Rust
trait Greeter {
    fn name(&self) -> String;

    fn greet(&self) -> String {
        format!("Hello, {}!", self.name())
    }
}

struct English;
struct Pirate;

impl Greeter for English {
    fn name(&self) -> String {
        String::from("friend")
    }
}

impl Greeter for Pirate {
    fn name(&self) -> String {
        String::from("matey")
    }

    fn greet(&self) -> String {
        format!("Ahoy, {}!", self.name())
    }
}

fn main() {
    println!("{}", English.greet());
    println!("{}", Pirate.greet());
}
Output
Hello, friend!
Ahoy, matey!

English and Pirate are unit structs, so English is both the type and its only value.

4. Default methods built on required methods

The most powerful use of defaults: implement one or two small methods, and get a whole family of behavior. The standard library's Iterator trait works exactly like this: implement next, and you get over 70 methods.

Rust
trait Scored {
    fn score(&self) -> u32;

    fn grade(&self) -> char {
        match self.score() {
            90.. => 'A',
            80..=89 => 'B',
            70..=79 => 'C',
            _ => 'F',
        }
    }

    fn passed(&self) -> bool {
        self.grade() != 'F'
    }

    fn report(&self) -> String {
        format!("score {} grade {} {}", self.score(), self.grade(), if self.passed() { "PASS" } else { "FAIL" })
    }
}

struct Exam {
    points: u32,
}

impl Scored for Exam {
    fn score(&self) -> u32 {
        self.points
    }
}

fn main() {
    for p in [95, 82, 40] {
        println!("{}", Exam { points: p }.report());
    }
}
Output
score 95 grade A PASS
score 82 grade B PASS
score 40 grade F FAIL

5. Taking any type that implements a trait: impl Trait

item: &impl Describe means "a reference to any type that implements Describe".

Rust
trait Describe {
    fn describe(&self) -> String;
}

struct Book {
    title: String,
}

struct Podcast {
    episode: u32,
}

impl Describe for Book {
    fn describe(&self) -> String {
        format!("book '{}'", self.title)
    }
}

impl Describe for Podcast {
    fn describe(&self) -> String {
        format!("podcast episode {}", self.episode)
    }
}

fn announce(item: &impl Describe) {
    println!("New: {}", item.describe());
}

fn main() {
    announce(&Book { title: String::from("Rust in Action") });
    announce(&Podcast { episode: 42 });
}
Output
New: book 'Rust in Action'
New: podcast episode 42

Behind the scenes, the compiler generates a separate copy of announce for each type it's called with, so each call is a direct, inlinable function call. That is static dispatch, and it is explained in Generics.

6. Passing a type that doesn't implement the trait

Rust✗ does not compile · E0277
trait Describe {
    fn describe(&self) -> String;
}

struct Book;

fn announce(item: &impl Describe) {
    println!("{}", item.describe());
}

fn main() {
    announce(&Book);
}
Output
error[E0277]: the trait bound `Book: Describe` is not satisfied
  --> src/main.rs:12:14
   |
12 |     announce(&Book);
   |     -------- ^^^^^ unsatisfied trait bound
   |     |
   |     required by a bound introduced by this call
   |
help: the trait `Describe` is not implemented for `Book`
  --> src/main.rs:5:1
   |
 5 | struct Book;
   | ^^^^^^^^^^^
help: this trait has no implementations, consider adding one
  --> src/main.rs:1:1
   |
 1 | trait Describe {
   | ^^^^^^^^^^^^^^
note: required by a bound in `announce`
  --> src/main.rs:7:25
   |
 7 | fn announce(item: &impl Describe) {
   |                         ^^^^^^^^ required by this bound in `announce`

7. Traits with associated functions

A trait can require functions without self, such as constructors. They are called on the type: T::new() or Type::name().

Rust
trait Named {
    fn type_name() -> &'static str;
    fn create_default() -> Self;
}

#[derive(Debug)]
struct Config {
    retries: u32,
}

impl Named for Config {
    fn type_name() -> &'static str {
        "Config"
    }

    fn create_default() -> Self {
        Config { retries: 3 }
    }
}

fn main() {
    println!("{}: {:?}", Config::type_name(), Config::create_default());
}
Output
Config: Config { retries: 3 }

Self in a trait means "the type implementing this trait".

8. Traits with associated constants

Rust
trait Currency {
    const CODE: &'static str;
    const DECIMALS: u32;

    fn format(amount_minor: i64) -> String {
        let divisor = 10_i64.pow(Self::DECIMALS);
        if Self::DECIMALS == 0 {
            format!("{} {}", amount_minor, Self::CODE)
        } else {
            format!("{}.{:0width$} {}", amount_minor / divisor, amount_minor % divisor, Self::CODE, width = Self::DECIMALS as usize)
        }
    }
}

struct Euro;
struct Yen;

impl Currency for Euro {
    const CODE: &'static str = "EUR";
    const DECIMALS: u32 = 2;
}

impl Currency for Yen {
    const CODE: &'static str = "JPY";
    const DECIMALS: u32 = 0;
}

fn main() {
    println!("{}", Euro::format(123_456));
    println!("{}", Yen::format(123_456));
}
Output
1234.56 EUR
123456 JPY

9. Implementing a trait for a type you didn't write

You can implement your own traits for standard types such as i32, str, or Vec<T>. This is called an extension trait, and it lets you add methods to existing types.

Rust
trait Percent {
    fn percent_of(self, total: Self) -> f64;
}

impl Percent for u32 {
    fn percent_of(self, total: u32) -> f64 {
        if total == 0 { 0.0 } else { self as f64 * 100.0 / total as f64 }
    }
}

trait Shout {
    fn shout(&self) -> String;
}

impl Shout for str {
    fn shout(&self) -> String {
        format!("{}!", self.to_uppercase())
    }
}

fn main() {
    println!("{:.1}%", 37_u32.percent_of(50));
    println!("{}", "deploy finished".shout());
    let owned = String::from("works on String too");
    println!("{}", owned.shout()); // String derefs to str
}
Output
74.0%
DEPLOY FINISHED!
WORKS ON STRING TOO!

10. The trait must be in scope to call its methods

Trait methods are only callable where the trait is visible. In one file that is automatic; across modules you bring a trait into scope with use, even if you never mention its name directly.

Rust
mod text_utils {
    pub trait WordCount {
        fn word_count(&self) -> usize;
    }

    impl WordCount for str {
        fn word_count(&self) -> usize {
            self.split_whitespace().count()
        }
    }
}

use text_utils::WordCount; // without this line, `.word_count()` is not found

fn main() {
    println!("{}", "traits must be in scope".word_count());
}
Output
5

This is why you will often see lines like use std::io::Write; or use std::fmt::Write; at the top of Rust files: they enable methods.

11. The orphan rule

You can implement a trait for a type only if the trait or the type is defined in your crate. You cannot implement a standard trait for a standard type, because two libraries could both do it and conflict.

Rust✗ does not compile · E0117
use std::fmt;

impl fmt::Display for Vec<i32> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} numbers", self.len())
    }
}

fn main() {}
Output
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
 --> src/main.rs:3:1
  |
3 | impl fmt::Display for Vec<i32> {
  | ^^^^^^^^^^^^^^^^^^^^^^--------
  |                       |
  |                       `Vec` is not defined in the current crate
  |
  = note: impl doesn't have any local type before any uncovered type parameters
  = note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
  = note: define and implement a trait or new type instead

The workaround is the newtype pattern: wrap the foreign type in your own struct and implement the trait for that. Advanced Traits shows it.

12. Traits can call each other through Self

Rust
trait Area {
    fn area(&self) -> f64;
}

trait Cost {
    fn cost_per_unit_area(&self) -> f64;
}

struct Floor {
    width_m: f64,
    length_m: f64,
    tile_price_per_m2: f64,
}

impl Area for Floor {
    fn area(&self) -> f64 {
        self.width_m * self.length_m
    }
}

impl Cost for Floor {
    fn cost_per_unit_area(&self) -> f64 {
        self.tile_price_per_m2
    }
}

impl Floor {
    fn total_cost(&self) -> f64 {
        self.area() * self.cost_per_unit_area()
    }
}

fn main() {
    let kitchen = Floor { width_m: 3.5, length_m: 4.0, tile_price_per_m2: 42.0 };
    println!("{} m2, {:.2} total", kitchen.area(), kitchen.total_cost());
}
Output
14 m2, 588.00 total

A type can implement any number of traits.

13. Implementing a standard trait: Display

Display controls how {} prints a value. The derive shortcut does not exist for Display, because there is no single right way to show a type to users.

Rust
use std::fmt;

struct Money {
    cents: i64,
    currency: &'static str,
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let sign = if self.cents < 0 { "-" } else { "" };
        let abs = self.cents.abs();
        write!(f, "{sign}{}.{:02} {}", abs / 100, abs % 100, self.currency)
    }
}

fn main() {
    let price = Money { cents: 129_99, currency: "USD" };
    let refund = Money { cents: -5_00, currency: "USD" };
    println!("{price}");
    println!("refund of {refund}");
    let text = price.to_string(); // every Display type gets to_string()
    println!("{} characters", text.len());
}
Output
129.99 USD
refund of -5.00 USD
10 characters

write! works like format!, but writes into the formatter. fmt::Result reports whether writing succeeded. The next lesson covers Display and the other standard traits in depth.

14. impl Trait in return position

A function can promise "I return some type implementing this trait" without naming the type.

Rust
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
}

struct Circle {
    r: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.r * self.r
    }
    fn name(&self) -> String {
        format!("circle r={}", self.r)
    }
}

fn unit_circle() -> impl Shape {
    Circle { r: 1.0 }
}

fn main() {
    let s = unit_circle();
    println!("{}: {:.4}", s.name(), s.area());
}
Output
circle r=1: 3.1416

The function must still return one concrete type. Returning a Circle on one path and a Square on another needs a trait object, covered in Trait Objects.

15. Several implementations, one function

Rust
trait Notifier {
    fn send(&self, to: &str, message: &str) -> String;
}

struct Email {
    from: String,
}

struct Sms;

struct Slack {
    channel: String,
}

impl Notifier for Email {
    fn send(&self, to: &str, message: &str) -> String {
        format!("email {} -> {to}: {message}", self.from)
    }
}

impl Notifier for Sms {
    fn send(&self, to: &str, message: &str) -> String {
        let short: String = message.chars().take(20).collect();
        format!("sms -> {to}: {short}")
    }
}

impl Notifier for Slack {
    fn send(&self, to: &str, message: &str) -> String {
        format!("slack #{} @{to}: {message}", self.channel)
    }
}

fn alert(n: &impl Notifier, on_call: &str) {
    println!("{}", n.send(on_call, "Disk usage above 90% on db-1"));
}

fn main() {
    alert(&Email { from: String::from("alerts@corp.com") }, "ops@corp.com");
    alert(&Sms, "+15550100");
    alert(&Slack { channel: String::from("incidents") }, "oncall");
}
Output
email alerts@corp.com -> ops@corp.com: Disk usage above 90% on db-1
sms -> +15550100: Disk usage above 90%
slack #incidents @oncall: Disk usage above 90% on db-1

16. A trait for mutable behavior

Trait methods can take &mut self too.

Rust
trait Counter {
    fn increment(&mut self);
    fn value(&self) -> u64;

    fn increment_by(&mut self, n: u64) {
        for _ in 0..n {
            self.increment();
        }
    }
}

struct Simple {
    n: u64,
}

struct Saturating {
    n: u64,
    max: u64,
}

impl Counter for Simple {
    fn increment(&mut self) {
        self.n += 1;
    }
    fn value(&self) -> u64 {
        self.n
    }
}

impl Counter for Saturating {
    fn increment(&mut self) {
        if self.n < self.max {
            self.n += 1;
        }
    }
    fn value(&self) -> u64 {
        self.n
    }
}

fn run(c: &mut impl Counter) -> u64 {
    c.increment_by(7);
    c.value()
}

fn main() {
    println!("{}", run(&mut Simple { n: 0 }));
    println!("{}", run(&mut Saturating { n: 0, max: 5 }));
}
Output
7
5

17. A trait that consumes self

Rust
trait IntoReport {
    fn into_report(self) -> String;
}

struct TestRun {
    passed: u32,
    failed: Vec<String>,
}

impl IntoReport for TestRun {
    fn into_report(self) -> String {
        let mut out = format!("{} passed, {} failed", self.passed, self.failed.len());
        for name in self.failed {
            out.push_str(&format!("\n  FAILED {name}"));
        }
        out
    }
}

fn main() {
    let run = TestRun { passed: 41, failed: vec![String::from("login_rejects_bad_password")] };
    println!("{}", run.into_report());
}
Output
41 passed, 1 failed
  FAILED login_rejects_bad_password

By convention, methods named into_* consume self, as_* borrow cheaply, and to_* create a new owned value.

18. Default methods can't access fields

A trait knows nothing about the fields of the types implementing it. Default methods can only use other trait methods.

Rust✗ does not compile · E0609
trait Named {
    fn greet(&self) -> String {
        format!("hi {}", self.name)
    }
}

fn main() {}
Output
error[E0609]: no field `name` on type `&Self`
 --> src/main.rs:3:31
  |
1 | trait Named {
  | ----------- type parameter 'Self' declared here
2 |     fn greet(&self) -> String {
3 |         format!("hi {}", self.name)
  |                               ^^^^ unknown field

Require an accessor method instead: fn name(&self) -> &str; and call self.name() in the default method.

19. Traits on enums

Enums implement traits exactly like structs, usually with a match inside each method.

Rust
trait Priced {
    fn price_cents(&self) -> u64;
}

enum Subscription {
    Free,
    Monthly { seats: u32 },
    Yearly { seats: u32 },
}

impl Priced for Subscription {
    fn price_cents(&self) -> u64 {
        match self {
            Subscription::Free => 0,
            Subscription::Monthly { seats } => 1_200 * *seats as u64,
            Subscription::Yearly { seats } => 12_000 * *seats as u64 / 12,
        }
    }
}

fn main() {
    let plans = [Subscription::Free, Subscription::Monthly { seats: 5 }, Subscription::Yearly { seats: 5 }];
    for p in &plans {
        println!("{} cents per month", p.price_cents());
    }
}
Output
0 cents per month
6000 cents per month
5000 cents per month

20. Shared data and hooks with composition

For "shared fields + shared code + abstract hooks", Rust splits the design into a struct for the shared data and a trait for the hooks, and uses composition.

Rust
trait Step {
    fn run_step(&self) -> String;
}

struct Job<S: Step> {
    name: String,
    step: S,
}

impl<S: Step> Job<S> {
    fn run(&self) {
        println!("start {}", self.name);
        println!("  {}", self.step.run_step());
        println!("end {}", self.name);
    }
}

struct Backup;
impl Step for Backup {
    fn run_step(&self) -> String {
        String::from("copying files")
    }
}

fn main() {
    let job = Job { name: String::from("nightly-backup"), step: Backup };
    job.run();
}
Output
start nightly-backup
  copying files
end nightly-backup

Job<S: Step> is a generic struct: it works for any step type S that implements Step. The next lessons explain generics fully.

21. Realistic example: pluggable pricing strategies

The strategy pattern is a natural fit for traits.

Rust
trait PricingStrategy {
    fn name(&self) -> String;
    fn price(&self, base_cents: u64, quantity: u32) -> u64;
}

struct Regular;

struct Bulk {
    min_quantity: u32,
    percent_off: u64,
}

struct Clearance {
    fixed_cents: u64,
}

impl PricingStrategy for Regular {
    fn name(&self) -> String {
        String::from("regular")
    }
    fn price(&self, base: u64, qty: u32) -> u64 {
        base * qty as u64
    }
}

impl PricingStrategy for Bulk {
    fn name(&self) -> String {
        format!("bulk ({}% off from {})", self.percent_off, self.min_quantity)
    }
    fn price(&self, base: u64, qty: u32) -> u64 {
        let full = base * qty as u64;
        if qty >= self.min_quantity { full - full * self.percent_off / 100 } else { full }
    }
}

impl PricingStrategy for Clearance {
    fn name(&self) -> String {
        String::from("clearance")
    }
    fn price(&self, _base: u64, qty: u32) -> u64 {
        self.fixed_cents * qty as u64
    }
}

fn quote(strategy: &impl PricingStrategy, base: u64, qty: u32) {
    println!("{:<24} {:>3} x {:>5} = {:>7}", strategy.name(), qty, base, strategy.price(base, qty));
}

fn main() {
    quote(&Regular, 1_999, 12);
    quote(&Bulk { min_quantity: 10, percent_off: 15 }, 1_999, 12);
    quote(&Bulk { min_quantity: 10, percent_off: 15 }, 1_999, 4);
    quote(&Clearance { fixed_cents: 500 }, 1_999, 12);
}
Output
regular                   12 x  1999 =   23988
bulk (15% off from 10)    12 x  1999 =   20390
bulk (15% off from 10)     4 x  1999 =    7996
clearance                 12 x  1999 =    6000

22. Realistic example: a storage abstraction

Code that depends on a trait instead of a concrete store can be tested with an in-memory fake.

Rust
trait KeyValueStore {
    fn get(&self, key: &str) -> Option<String>;
    fn set(&mut self, key: &str, value: &str);

    fn get_or(&self, key: &str, default: &str) -> String {
        self.get(key).unwrap_or_else(|| default.to_string())
    }
}

struct MemoryStore {
    entries: Vec<(String, String)>,
}

impl KeyValueStore for MemoryStore {
    fn get(&self, key: &str) -> Option<String> {
        self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
    }

    fn set(&mut self, key: &str, value: &str) {
        match self.entries.iter_mut().find(|(k, _)| k == key) {
            Some(entry) => entry.1 = value.to_string(),
            None => self.entries.push((key.to_string(), value.to_string())),
        }
    }
}

fn record_login(store: &mut impl KeyValueStore, user: &str) -> u32 {
    let key = format!("logins:{user}");
    let count: u32 = store.get_or(&key, "0").parse().unwrap_or(0) + 1;
    store.set(&key, &count.to_string());
    count
}

fn main() {
    let mut store = MemoryStore { entries: Vec::new() };
    record_login(&mut store, "ana");
    record_login(&mut store, "ben");
    let n = record_login(&mut store, "ana");
    println!("ana has logged in {n} times");
    println!("{:?}", store.entries);
}
Output
ana has logged in 2 times
[("logins:ana", "2"), ("logins:ben", "1")]

23. Realistic example: validation rules as traits

Each rule is a small type implementing one trait. New rules can be added without touching the others.

Rust
trait Rule {
    fn check(&self, password: &str) -> Result<(), String>;
}

struct MinLength(usize);
struct RequiresDigit;
struct NotContaining(&'static str);

impl Rule for MinLength {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.chars().count() >= self.0 { Ok(()) } else { Err(format!("must be at least {} characters", self.0)) }
    }
}

impl Rule for RequiresDigit {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.chars().any(|c| c.is_ascii_digit()) { Ok(()) } else { Err(String::from("must contain a digit")) }
    }
}

impl Rule for NotContaining {
    fn check(&self, p: &str) -> Result<(), String> {
        if p.to_lowercase().contains(self.0) { Err(format!("must not contain '{}'", self.0)) } else { Ok(()) }
    }
}

fn validate(p: &str, r1: &impl Rule, r2: &impl Rule, r3: &impl Rule) -> Vec<String> {
    [r1.check(p), r2.check(p), r3.check(p)].into_iter().filter_map(|r| r.err()).collect()
}

fn main() {
    for pw in ["password", "Tr0ub4dor&3", "hunter2"] {
        let errors = validate(pw, &MinLength(8), &RequiresDigit, &NotContaining("password"));
        println!("{pw:>12}: {:?}", errors);
    }
}
Output
    password: ["must contain a digit", "must not contain 'password'"]
 Tr0ub4dor&3: []
     hunter2: ["must be at least 8 characters"]

Passing each rule as a separate parameter is clumsy. A list of rules of different types needs Vec<Box<dyn Rule>>, which you'll build in Trait Objects.

24. Realistic example: exporting data in several formats

Rust
struct Employee {
    name: String,
    role: String,
    salary: u32,
}

trait Exporter {
    fn header(&self) -> String;
    fn row(&self, e: &Employee) -> String;
    fn footer(&self) -> String {
        String::new()
    }

    fn export(&self, staff: &[Employee]) -> String {
        let mut out = self.header();
        for e in staff {
            out.push_str(&self.row(e));
        }
        out.push_str(&self.footer());
        out
    }
}

struct Csv;
struct Markdown;

impl Exporter for Csv {
    fn header(&self) -> String {
        String::from("name,role,salary\n")
    }
    fn row(&self, e: &Employee) -> String {
        format!("{},{},{}\n", e.name, e.role, e.salary)
    }
}

impl Exporter for Markdown {
    fn header(&self) -> String {
        String::from("| Name | Role | Salary |\n|---|---|---|\n")
    }
    fn row(&self, e: &Employee) -> String {
        format!("| {} | {} | {} |\n", e.name, e.role, e.salary)
    }
    fn footer(&self) -> String {
        String::from("\n_generated report_\n")
    }
}

fn main() {
    let staff = vec![
        Employee { name: String::from("Ana"), role: String::from("Engineer"), salary: 9_000 },
        Employee { name: String::from("Ben"), role: String::from("Designer"), salary: 7_500 },
    ];
    print!("{}", Csv.export(&staff));
    println!();
    print!("{}", Markdown.export(&staff));
}
Output
name,role,salary
Ana,Engineer,9000
Ben,Designer,7500

| Name | Role | Salary |
|---|---|---|
| Ana | Engineer | 9000 |
| Ben | Designer | 7500 |

_generated report_

export is a template method: the trait defines the algorithm, and each implementation fills in the steps.

25. Realistic example: an event handler trait

Rust
#[derive(Debug)]
enum Event {
    UserSignedUp { email: String },
    OrderPlaced { id: u32, total_cents: u64 },
    PaymentFailed { order_id: u32 },
}

trait Handler {
    fn name(&self) -> &'static str;
    fn handle(&mut self, event: &Event) -> Option<String>;
}

struct WelcomeMailer {
    sent: u32,
}

struct FraudCheck {
    threshold_cents: u64,
}

impl Handler for WelcomeMailer {
    fn name(&self) -> &'static str {
        "welcome-mailer"
    }
    fn handle(&mut self, event: &Event) -> Option<String> {
        if let Event::UserSignedUp { email } = event {
            self.sent += 1;
            return Some(format!("welcome email #{} to {email}", self.sent));
        }
        None
    }
}

impl Handler for FraudCheck {
    fn name(&self) -> &'static str {
        "fraud-check"
    }
    fn handle(&mut self, event: &Event) -> Option<String> {
        match event {
            Event::OrderPlaced { id, total_cents } if *total_cents > self.threshold_cents => {
                Some(format!("order {id} flagged for review"))
            }
            Event::PaymentFailed { order_id } => Some(format!("order {order_id}: payment failed, watch account")),
            _ => None,
        }
    }
}

fn dispatch(events: &[Event], handler: &mut impl Handler) {
    for e in events {
        if let Some(action) = handler.handle(e) {
            println!("[{}] {action}", handler.name());
        }
    }
}

fn main() {
    let events = vec![
        Event::UserSignedUp { email: String::from("ana@example.com") },
        Event::OrderPlaced { id: 1, total_cents: 4_500 },
        Event::OrderPlaced { id: 2, total_cents: 250_000 },
        Event::PaymentFailed { order_id: 2 },
        Event::UserSignedUp { email: String::from("ben@example.com") },
    ];
    dispatch(&events, &mut WelcomeMailer { sent: 0 });
    dispatch(&events, &mut FraudCheck { threshold_cents: 100_000 });
}
Output
[welcome-mailer] welcome email #1 to ana@example.com
[welcome-mailer] welcome email #2 to ben@example.com
[fraud-check] order 2 flagged for review
[fraud-check] order 2: payment failed, watch account

26. Realistic example: extending Vec with statistics

An extension trait adds domain-specific methods to a standard type, without wrapping it.

Rust
trait Stats {
    fn mean(&self) -> Option<f64>;
    fn percentile(&self, p: f64) -> Option<f64>;
}

impl Stats for [f64] {
    fn mean(&self) -> Option<f64> {
        if self.is_empty() {
            return None;
        }
        Some(self.iter().sum::<f64>() / self.len() as f64)
    }

    fn percentile(&self, p: f64) -> Option<f64> {
        if self.is_empty() {
            return None;
        }
        let mut sorted = self.to_vec();
        sorted.sort_by(|a, b| a.total_cmp(b));
        let rank = (p / 100.0 * (sorted.len() - 1) as f64).round() as usize;
        Some(sorted[rank])
    }
}

fn main() {
    let latencies_ms = vec![12.0, 15.0, 11.0, 250.0, 14.0, 13.0, 16.0, 12.0, 18.0, 900.0];
    println!("mean: {:?}", latencies_ms.mean());
    println!("p50:  {:?}", latencies_ms.percentile(50.0));
    println!("p90:  {:?}", latencies_ms.percentile(90.0));
    let empty: Vec<f64> = Vec::new();
    println!("empty mean: {:?}", empty.mean());
}
Output
mean: Some(126.1)
p50:  Some(15.0)
p90:  Some(250.0)
empty mean: None

Implementing the trait for the slice type [f64] makes it work on arrays, vectors, and slices alike, because Vec<f64> automatically derefs to [f64].

Summary

  • trait Name { fn method(&self) -> T; } declares shared behavior; impl Name for Type { ... } implements it.
  • Default methods give implementors behavior for free, built on the required methods.
  • &impl Trait parameters accept any implementing type, with static dispatch.
  • Traits can have associated functions and constants. Self means the implementing type.
  • You can implement your traits for any type (extension traits), and any trait for your types. The orphan rule forbids foreign traits on foreign types.
  • Trait methods need the trait in scope (use).
  • Traits have no fields. Share data through composition, not inheritance.

Next: Deriving Standard Traits.