Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9. Traits and Generics

A generic function works for many types. A trait says what a type can do. The two are one mechanism: a generic function states which traits its types must implement, and the compiler checks that before generating any code. Nearly everything the standard library offers is expressed this way, so you cannot read real Rust without reading traits.

9.1 Generic Functions

Write the type as a parameter and use it in the signature:

fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
    let mut it = items.iter();
    let mut best = it.next()?;
    for item in it {
        if item > best {
            best = item;
        }
    }
    Some(best)
}

fn main() {
    println!("{:?}", largest(&[3, 9, 2, 7]));
    println!("{:?}", largest(&["fig", "apple", "pear"]));
    println!("{:?}", largest(&[1.5, 0.5]));
    println!("{:?}", largest::<i32>(&[]));
}
Some(9)
Some("pear")
Some(1.5)
None

<T: PartialOrd> introduces a type parameter and a bound: T can be any type that can be compared with <. Without the bound the function does not compile, because nothing is known about T and > might not exist for it. This is the difference from C++ templates, where the same function compiles and then fails inside the body when instantiated with the wrong type. In Rust the signature is a contract checked once.

The compiler produces a separate copy of the function for each type actually used, so there is no dispatch at runtime and no penalty against a hand-written version. Note also that the empty case needed a type, largest::<i32>(&[]), because nothing in the call says what the elements would have been.

Structs can be generic too, and the impl block repeats the parameter:

struct Pair<T> {
    first: T,
    second: T,
}

impl<T: PartialOrd + Copy> Pair<T> {
    fn larger(&self) -> T {
        if self.first > self.second { self.first } else { self.second }
    }
}

Multiple bounds are joined with +. When the list gets long, move it to a where clause, the same thing with more room:

fn report<T>(items: &[T]) -> String
where
    T: fmt::Display + Clone,
{
    ...
}

9.2 Defining a Trait

A trait is a set of method signatures, and may supply default bodies:

trait Shape {
    fn area(&self) -> f64;

    // A default method: implementors may override it or take this one.
    fn describe(&self) -> String {
        format!("a shape of area {:.2}", self.area())
    }
}

Implementing it for a type is a separate block:

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }

    fn describe(&self) -> String {
        format!("a circle of radius {}", self.radius)
    }
}

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

Circle overrides describe and Square takes the default:

a circle of radius 1
a shape of area 4.00

The trait and the type are declared separately, which means you can implement your own trait for a type from the standard library, or implement a standard trait for your own type. What you cannot do is implement someone else’s trait for someone else’s type, since two crates could then disagree; that is the orphan rule, and the way around it is to wrap the foreign type in a tuple struct of your own.

9.3 Static and Dynamic Dispatch

A function that takes any shape can be written two ways.

fn print_area(s: &impl Shape) {
    println!("{:.4}", s.area());
}

&impl Shape is shorthand for a generic parameter with a bound, and it is resolved at compile time: one copy of the function per type, with the call inlined. Fast, and the type is fixed at each call site.

When you need a collection holding different types at once, you need the other kind:

let shapes: Vec<Box<dyn Shape>> = vec![
    Box::new(Circle { radius: 2.0 }),
    Box::new(Square { side: 3.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();

dyn Shape is a trait object: a value of some type that implements Shape, with the identity forgotten. It is a pair of pointers, one to the data and one to a table of the type’s implementations, and the call goes through that table. This is what other languages call an interface, and it costs an indirection.

Choose static dispatch by default and reach for dyn when the set of types is open or is decided at runtime. When the set is small and closed, an enum, as in Chapter 6, is often better than either: no allocation, no indirection, and the compiler checks that every match covers every case.

9.4 Traits You Already Use

Most of the standard library is traits, and recognizing the important ones is most of what “learning the library” means.

TraitWhat it gives you
Debug{:?}, derived, for programmers
Display{}, written by hand, for users
Clone, Copy.clone(), and copy-on-assign
PartialEq, Eq==
PartialOrd, Ord<, sorting, .max()
Hashuse as a HashMap key
DefaultType::default()
From, Intoconversions, and ?
Iteratorfor loops and every adapter
Dropcleanup at end of scope

derive writes the mechanical ones for you. The interesting ones are written by hand.

Display is one of those, because only you know how your type should read:

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

write! is println! aimed at a destination rather than at standard output, and returns the fmt::Result the method owes. Implementing Display also gets you .to_string() at no extra cost, because there is a blanket implementation in the library that says so.

From is the other one worth writing. Implement it and you get .into() in the other direction for free, and ? learns how to convert your error types:

use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Celsius(f64);

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Fahrenheit(f64);

impl From<Celsius> for Fahrenheit {
    fn from(c: Celsius) -> Fahrenheit {
        Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
    }
}

impl fmt::Display for Celsius {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°C", self.0)
    }
}

impl fmt::Display for Fahrenheit {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:.1}°F", self.0)
    }
}

fn main() {
    let boiling = Celsius(100.0);
    let f: Fahrenheit = boiling.into();
    println!("{boiling} is {f}");
    println!("{}", Fahrenheit::from(Celsius(-40.0)));
    println!("{}", Celsius(21.0) > Celsius(18.0));
}
100.0°C is 212.0°F
-40.0°F
true

Deriving PartialOrd on a tuple struct compares the field, so Celsius values order themselves, and a Vec<Celsius> sorts. This is the temperature program from Chapter 1 with the mistakes made impossible: a Fahrenheit cannot be passed where a Celsius belongs, and the conversion has one definition.

9.5 Implementing Iterator

Implement one method and the entire adapter library applies to your type:

struct Fib {
    a: u64,
    b: u64,
}

impl Iterator for Fib {
    type Item = u64;

    fn next(&mut self) -> Option<u64> {
        let out = self.a;
        self.a = self.b;
        self.b = out + self.b;
        Some(out)
    }
}

fn fib() -> Fib {
    Fib { a: 0, b: 1 }
}

fn main() {
    let first: Vec<u64> = fib().take(10).collect();
    println!("{first:?}");

    let even_sum: u64 = fib().take_while(|n| *n < 1000).filter(|n| n % 2 == 0).sum();
    println!("{even_sum}");

    for (i, n) in fib().skip(50).enumerate().take(3) {
        println!("{} {n}", i + 50);
    }
}
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
798
50 12586269025
51 20365011074
52 32951280099

type Item = u64 is an associated type: a type the implementation supplies rather than the caller. next returns None to finish, and this iterator never does, which is why every use of it is bounded by take or take_while. The fiftieth Fibonacci number arrives without the first fifty being stored anywhere.

This is the pattern to imitate whenever you write something that produces a sequence. Write next, get map, filter, zip, sum, collect, and the for loop, all of them compiled down to the loop you would have written by hand.

9.6 What Traits Are Not

Traits are not classes. There is no inheritance of data, no base type, and no overriding of one implementation by another. A trait may require another trait, written trait Ord: PartialOrd, which means an implementor must supply both, but that is a requirement rather than inheritance.

Nor are traits duck typing. Everything is checked when the code is compiled, and a missing implementation is an error at the call, naming the trait and the type. If you are coming from Python, the mental move is from “it will work if the method happens to exist” to “say what you need and the compiler guarantees it”.

Exercise 9-1. Add a perimeter method to Shape with no default, and find out what the compiler says about the existing implementations.

Exercise 9-2. Write a trait Summary with a default summarize that uses a required title method. Implement it for two types, overriding the default in one.

Exercise 9-3. Implement Iterator for a type that yields the lines of a string in reverse order.

Exercise 9-4. Write fn print_all<T: fmt::Display>(items: &[T]). Then rewrite it with impl Trait, and again with a where clause. All three compile to the same thing; which reads best?

Exercise 9-5. Chapter 6 modeled shapes as an enum and this chapter modeled them as a trait. Write down the change each version makes easy and the change each makes hard, then decide which you would use for a drawing program and which for a geometry library.