Traits and Generics
Shared behavior with traits, generic functions and types with trait bounds, static dispatch by monomorphization versus trait objects with dyn, derive, and the standard traits every type meets.
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. -
whereclause: the same bounds after the signature, easier to read when there are several type parameters or long bounds. -
impl Traitin 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.
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.