Traits: shared behavior

A trait declares methods that a type can implement. Code that needs "something with a price" asks for the trait, not for a particular type. In this example a fixed tariff and a day/night tariff both implement a Price trait (the prices are Kestrel's tariffs, €0.2700 fixed and €0.3120 day / €0.2240 night; the night share is an example value):

// Prices in ten-thousandths of a euro per kWh (2700 = €0.2700).
trait Price {
    fn cost_cents(&self, kwh: u64) -> u64;

    // A default method: every implementor gets it for free.
    fn describe(&self, kwh: u64) -> String {
        let cents = self.cost_cents(kwh);
        format!("{kwh} kWh costs €{}.{:02}", cents / 100, cents % 100)
    }
}

struct Fixed {
    per_kwh: u64,
}

struct DayNight {
    day: u64,
    night: u64,
    night_share_percent: u64,
}

impl Price for Fixed {
    fn cost_cents(&self, kwh: u64) -> u64 {
        kwh * self.per_kwh / 100
    }
}

impl Price for DayNight {
    fn cost_cents(&self, kwh: u64) -> u64 {
        let night_kwh = kwh * self.night_share_percent / 100;
        let day_kwh = kwh - night_kwh;
        (day_kwh * self.day + night_kwh * self.night) / 100
    }
}

fn main() {
    let fixed = Fixed { per_kwh: 2700 };
    let day_night = DayNight { day: 3120, night: 2240, night_share_percent: 40 }; // share: an example
    println!("fixed:     {}", fixed.describe(350));
    println!("day/night: {}", day_night.describe(350));
}
fixed:     350 kWh costs €94.50
day/night: 350 kWh costs €96.88

impl Price for Fixed is an impl block for a trait (see impl blocks): it must provide every method the trait declares without a body, and may override the ones with a default body, such as describe. A method from a trait can only be called when the trait is in scope. For the traits in the standard prelude (such as Clone, Iterator, ToString, From and Into) that is automatic; for every other trait, including standard ones such as std::io::Write, you add a use line.

The standard traits every type meets

Much of what looks like built-in language behavior is a trait from the standard library. Printing with {} is Display, == is PartialEq, for loops use IntoIterator, and the ? operator converts errors with From. Implement the trait and your type joins in:

use std::fmt;

struct Wh(u64);

impl fmt::Display for Wh {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}.{:03} kWh", self.0 / 1000, self.0 % 1000)
    }
}

impl From<u32> for Wh {
    fn from(kwh: u32) -> Self {
        Wh(u64::from(kwh) * 1000)
    }
}

fn main() {
    let a = Wh(412_338);
    let b = Wh::from(350);
    let c: Wh = 120.into(); // Into comes for free with From
    println!("{a}, {b}, {c}");
    let text = b.to_string(); // ToString comes for free with Display
    println!("{text:?} is {} bytes", text.len());
}
412.338 kWh, 350.000 kWh, 120.000 kWh
"350.000 kWh" is 11 bytes

Implementing From<u32> for Wh also gives Into<Wh> for u32, and implementing Display gives .to_string(): the standard library contains generic implementations that cover every type with the first trait. The traits you will meet most:

Trait Provides Derive?
Debug {:?} for developers yes
Display {} and .to_string() for people no, write it
Clone, Copy explicit copy; implicit copy instead of a move (see Ownership) yes
PartialEq, Eq ==, != yes
PartialOrd, Ord <, >, sort(), BTreeMap keys yes
Hash HashMap and HashSet keys yes
Default T::default(), an "empty" value yes
From, Into conversions; ? uses From for errors no, write From
Iterator next() and dozens of adapters (see Iterators and Closures) no
Drop cleanup code when a value goes out of scope no
Error marks a type as an error (needs Debug and Display) no
Send, Sync safe to move or share between threads (see Concurrency) automatic

Derive: implementations the compiler writes

The derive attribute, #[derive(Debug, PartialEq, …)] above a struct or enum, asks the compiler to generate the implementation of each listed trait from the type's fields: equal if all fields are equal, ordered field by field, enum variants in the order they are declared. That is right most of the time. When it is not, you write the implementation yourself. In this example, meter ids should be equal regardless of case:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Tariff {
    Fixed,
    DayNight,
    Dynamic,
    Variable,
}

// Written by hand: meter ids are compared without regard to case.
#[derive(Debug)]
struct MeterId(String);

impl PartialEq for MeterId {
    fn eq(&self, other: &Self) -> bool {
        self.0.eq_ignore_ascii_case(&other.0)
    }
}

fn main() {
    let mut tariffs = vec![Tariff::Variable, Tariff::Fixed, Tariff::Dynamic, Tariff::DayNight];
    tariffs.sort(); // derived Ord: the order in which the variants are declared
    println!("{tariffs:?}");
    println!("{}", Tariff::Fixed < Tariff::Dynamic);

    let a = MeterId(String::from("M-30004"));
    let b = MeterId(String::from("m-30004"));
    println!("{a:?} == {b:?}: {}", a == b);
}
[Fixed, DayNight, Dynamic, Variable]
true
MeterId("M-30004") == MeterId("m-30004"): true

A derive only works if every field implements the trait too: a struct with an f64 field can derive PartialEq but not Eq, because NaN is not equal to itself. Derives are macros that run at compile time; crates can add their own, which is how libraries such as serde offer #[derive(Serialize)].

Generics: one function for many types

A generic type parameter, written in angle brackets as <T>, is a placeholder for a type that the caller chooses. You have used generic types all along: Vec<T>, Option<T> and HashMap<K, V> are each one definition that works for any element type. Your own functions can be generic too:

fn largest<T: PartialOrd + Copy>(items: &[T]) -> Option<T> {
    let mut largest = *items.first()?;
    for &item in items {
        if item > largest {
            largest = item;
        }
    }
    Some(largest)
}

fn main() {
    println!("{:?}", largest(&[350_u32, 412, 120]));
    println!("{:?}", largest(&[21.5_f64, 19.0, 22.25]));
    println!("{:?}", largest(&['k', 'W', 'h']));
    println!("{:?}", largest::<i64>(&[]));
}
Some(412)
Some(22.25)
Some('k')
None

The compiler infers T from the arguments (u32, f64, char); for the empty slice there is nothing to infer from, so ::<i64> (the "turbofish") names it. The 'k' result is correct, by the way: characters compare by code point, and all lowercase ASCII letters come after the uppercase ones.

Trait bounds: what a generic type must be able to do

Inside a generic function, the compiler knows nothing about T except what you promise. A trait bound such as T: PartialOrd is that promise: "whatever T is, it can be compared". Leave it out and the comparison is refused, with the missing bound spelled out in the help line:

fn largest<T: Copy>(items: &[T]) -> Option<T> {
    let mut largest = *items.first()?;
    for &item in items {
        if item > largest {
            largest = item;
        }
    }
    Some(largest)
}

fn main() {
    println!("{:?}", largest(&[350, 412, 120]));
}
example.rs:4:17: error[E0369]: binary operation `>` cannot be applied to type `T`
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> example.rs:4:17
  |
4 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider further restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: Copy + std::cmp::PartialOrd>(items: &[T]) -> Option<T> {
  |                    ++++++++++++++++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0369`.

This is the difference from C++ templates and from Python: a Rust generic function is checked once, against its bounds, not again for every type it is used with. If it compiles, it works for every type that meets the bounds, and a caller who passes a type without PartialOrd gets an error at the call, not deep inside the function.

Bounds can be written in three ways. They mean the same thing; pick the one that reads best:

use std::fmt::Display;

// A generic struct: the value can be any type.
struct Measurement<T> {
    meter: String,
    value: T,
}

impl<T: Display> Measurement<T> {
    fn label(&self, unit: &str) -> String {
        format!("{}: {} {unit}", self.meter, self.value)
    }
}

// Two ways to write the same bound.
fn print_all<T>(items: &[Measurement<T>], unit: &str)
where
    T: Display,
{
    for item in items {
        println!("{}", item.label(unit));
    }
}

fn shout(text: impl Display) -> String {
    format!("{text}!").to_uppercase()
}

fn main() {
    let power = [
        Measurement { meter: String::from("M-30004"), value: 1234_u32 },
        Measurement { meter: String::from("M-30007"), value: 980 },
    ];
    let voltage = [Measurement { meter: String::from("M-30004"), value: 231.4_f64 }];
    print_all(&power, "W");
    print_all(&voltage, "V");
    println!("{}", shout("meter offline"));
    println!("{}", shout(404));
}
M-30004: 1234 W
M-30007: 980 W
M-30004: 231.4 V
METER OFFLINE!
404!
  • Inline: fn f<T: Display + Clone>(…), short bounds; + combines traits.
  • where clause: the same bounds after the signature, easier to read when there are several type parameters or long bounds.
  • impl Trait in argument position: fn shout(text: impl Display) is shorthand for a generic parameter with that bound. In return position, fn readings() -> impl Iterator<Item = u32> means "some type that implements this trait, which I do not name"; that is common with iterators and closures, whose real types are long or cannot be written at all.

The bound on impl<T: Display> Measurement<T> applies to the methods in that block only: a Measurement<Vec<u32>> can still exist, it just has no label method, because a vector has no Display form.

Monomorphization and static dispatch

How does one generic function run on u16, u32 and u8? Through monomorphization: the compiler makes a separate copy of the function for each concrete type it is used with, with T replaced. The program below shows the three copies it made; type_name is a debugging aid that prints the type a copy was made for:

use std::any::type_name;

fn total<T: Copy + Into<u64>>(values: &[T]) -> u64 {
    println!("  running total::<{}>", type_name::<T>());
    values.iter().map(|&v| v.into()).sum()
}

fn main() {
    println!("{}", total(&[350_u16, 412, 120]));
    println!("{}", total(&[3_504_000_000_u32, 1]));
    println!("{}", total(&[7_u8, 8]));
}
  running total::<u16>
882
  running total::<u32>
3504000001
  running total::<u8>
15

Each call goes straight to the copy for its type, chosen at compile time: static dispatch. The optimizer sees the concrete type and can inline the code, so generic code runs as fast as code written by hand for each type. Rust's standard library relies on this: an iterator chain with closures compiles to roughly the loop you would have written yourself. The costs are compile time and binary size, because every type used means another copy. That is one reason Rust binaries are larger than a comparable C program; the biggest is that Rust links its standard library into every program.

Trait objects: choosing at run time

Generics need the type to be known at compile time. Sometimes it is not: a list of contracts holds fixed and dynamic tariffs mixed, and which is which comes from a database. A trait object, dyn Price, behind a pointer such as Box<dyn Price> or &dyn Price, stands for "some type that implements Price", and the method to call is looked up when the program runs:

trait Price {
    fn cost_cents(&self, kwh: u64) -> u64;
    fn name(&self) -> String;
}

struct Fixed {
    per_kwh: u64,
}

struct Dynamic {
    hourly: Vec<u64>, // price per kWh for each hour, ten-thousandths of a euro
}

impl Price for Fixed {
    fn cost_cents(&self, kwh: u64) -> u64 {
        kwh * self.per_kwh / 100
    }
    fn name(&self) -> String {
        String::from("fixed")
    }
}

impl Price for Dynamic {
    fn cost_cents(&self, kwh: u64) -> u64 {
        // simplification for the example: spread the kWh evenly over the hours
        let average = self.hourly.iter().sum::<u64>() / self.hourly.len() as u64;
        kwh * average / 100
    }
    fn name(&self) -> String {
        format!("dynamic ({} hourly prices)", self.hourly.len())
    }
}

fn main() {
    // One list, two different types behind the same trait.
    let contracts: Vec<Box<dyn Price>> = vec![
        Box::new(Fixed { per_kwh: 2700 }),
        Box::new(Dynamic { hourly: vec![2100, 1900, 2600, 3400] }), // example values
    ];
    for contract in &contracts {
        println!("{:28} {:>6} cents", contract.name(), contract.cost_cents(350));
    }
    println!("&Fixed:     {} bytes", size_of::<&Fixed>());
    println!("&dyn Price: {} bytes", size_of::<&dyn Price>());
}
fixed                          9450 cents
dynamic (4 hourly prices)      8750 cents
&Fixed:     8 bytes
&dyn Price: 16 bytes

The sizes, measured on a 64-bit Linux machine (ARM64), show how it works. A reference to a Fixed is one pointer. A reference to a dyn Price is two: one to the data and one to a vtable, a table of function pointers for that type's implementation of the trait. Calling contract.cost_cents(350) reads the right function from the table and calls it. That is dynamic dispatch, the same mechanism Java and C# use for every interface call; in Rust you only pay for it where you write dyn.

Static versus dynamic dispatch. On the left, a generic function bill with type parameter P is compiled into two copies, one for Fixed and one for DayNight, and each call jumps directly to its copy. On the right, a reference of type ampersand dyn Price is two pointers: one to the Fixed value's data and one to the vtable for Fixed implementing Price, which lists the addresses of cost_cents, name and drop. A call reads the function from the vtable at run time. static dispatch (generics) bill(&fixed) bill(&day_night) bill::<Fixed> bill::<DayNight> one compiled copy per type dynamic dispatch (dyn) &dyn Price data vtable Fixed { 2700 } vtable: Fixed as Price cost_cents name drop, size
Generics: the call target is fixed at compile time. Trait objects: a fat pointer carries a vtable, and the call target is read from it at run time.

Write dyn

The keyword dyn is required. Without it, the compiler assumes you meant a type, finds a trait, and tells you to add dyn:

trait Price {
    fn cost_cents(&self, kwh: u64) -> u64;
}

struct Fixed;

impl Price for Fixed {
    fn cost_cents(&self, kwh: u64) -> u64 {
        kwh * 27
    }
}

fn main() {
    let contract: Box<Price> = Box::new(Fixed);
    println!("{}", contract.cost_cents(350));
}
example.rs:14:23: error[E0782]: expected a type, found a trait

Not every trait can be used as dyn. The vtable needs one fixed function per method, so a trait whose methods return Self or have their own type parameters is not dyn compatible; the compiler reports error[E0038]: the trait `Tariff` is not dyn compatible and names the method that is the problem.

Generics or trait objects?

Aspect Generics (T: Price, impl Price) Trait objects (dyn Price)
Type chosen at compile time at run time
Mixed types in one collection no: a Vec<T> holds one type yes: Vec<Box<dyn Price>>
Call cost direct, can be inlined one lookup in the vtable, no inlining
Binary size one copy per type one copy
Restrictions none beyond the bounds the trait must be dyn compatible

Start with generics; reach for dyn when you need different types side by side, or a plug-in point where new types are added without recompiling the code that uses them.

Box<dyn Error> explained

The return type Result<T, Box<dyn Error>> from Error Handling is a trait object: "an error of any type that implements std::error::Error, on the heap". Because ? converts any such error into the box, one function can return errors of different types:

use std::error::Error;

fn parse_reading(line: &str) -> Result<(String, u32), Box<dyn Error>> {
    let (meter, kwh) = line.split_once(';').ok_or("expected meter;kwh")?;
    let kwh: u32 = kwh.parse()?;
    Ok((meter.to_string(), kwh))
}

fn main() {
    for line in ["M-30004;350", "M-30007", "M-30012;12O"] {
        match parse_reading(line) {
            Ok((meter, kwh)) => println!("{meter}: {kwh} kWh"),
            Err(e) => println!("{line:?}: {e}"),
        }
    }
}
M-30004: 350 kWh
"M-30007": expected meter;kwh
"M-30012;12O": invalid digit found in string

The first error is a plain string, which the standard library converts into a boxed error; the second is a ParseIntError. The caller can print either, but can no longer match on which kind it was. That trade-off is why libraries usually define their own error enum (as on Error Handling) while small applications are happy with Box<dyn Error>.

The orphan rule

You can implement your trait for any type, and any trait for your type. What you cannot do is implement a foreign trait for a foreign type: Display (from the standard library) for Vec<u32> (also from the standard library). This is the orphan rule:

use std::fmt;

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

fn main() {
    let readings = vec![350, 412, 120];
    println!("{readings}");
}
example.rs:3:1: error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate

The reason is coherence: for any type and trait there must be at most one implementation in the whole program. If two crates could each implement Display for Vec<u32>, a program using both would have two answers, and adding a dependency could break code that compiled before. The rule guarantees that only the crate that owns the trait or the type can write the implementation. The usual way around it is a newtype (see tuple structs), which is a local type:

use std::fmt;

// A newtype is a local type, so the orphan rule allows this impl.
struct Readings(Vec<u32>);

impl fmt::Display for Readings {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let total: u32 = self.0.iter().sum();
        write!(f, "{} readings, {total} kWh", self.0.len())
    }
}

fn main() {
    let readings = Readings(vec![350, 412, 120]);
    println!("{readings}");
}
3 readings, 882 kWh

The full rule in the Rust Reference has more detail for generic types (the compiler's note mentions "uncovered type parameters"), but "the trait or the type must be yours" covers almost every case you will meet.