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.
| Trait | What 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() |
Hash | use as a HashMap key |
Default | Type::default() |
From, Into | conversions, and ? |
Iterator | for loops and every adapter |
Drop | cleanup 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.