Generics and Trait Bounds
You have been using generic types since the collections lesson: Vec<i32>,
Option<String>, HashMap<String, u32> and Result<u8, ParseIntError> are
all one definition (Vec<T>, Option<T>, ...) filled in with different
types. Generics let you write that kind of code yourself: one function,
struct or enum that works for many types, checked by the compiler for each
type it is used with.
On its own, a generic type can't do much, because the compiler knows nothing
about it. Trait bounds fix that: they say "any type, as long as it
implements this trait", which unlocks the trait's methods. This lesson covers
generic functions, structs, enums and methods, trait bounds and where
clauses, the conversion traits From, Into and AsRef, and impl Trait in
argument and return position.
Generic functions
Here are two functions that do the same thing for two different types:
fn first_and_last_i32(items: &[i32]) -> (i32, i32) {
(items[0], items[items.len() - 1])
}
fn first_and_last_char(items: &[char]) -> (char, char) {
(items[0], items[items.len() - 1])
}
fn main() {
println!("{:?}", first_and_last_i32(&[4, 8, 15, 16]));
println!("{:?}", first_and_last_char(&['r', 'u', 's', 't']));
}(4, 16)
('r', 't')The bodies are identical; only the types differ. A generic function declares
a type parameter in angle brackets after its name, then uses it wherever
a concrete type would go. By convention type parameters are short
UpperCamelCase names, usually T (for "type"):
fn swap_pair<T>(pair: (T, T)) -> (T, T) {
let (a, b) = pair;
(b, a)
}
fn main() {
println!("{:?}", swap_pair((1, 2)));
println!("{:?}", swap_pair(("left", "right")));
println!("{:?}", swap_pair((true, false)));
}(2, 1)
("right", "left")
(false, true)swap_pair<T> reads "for any type T, take a pair of Ts and return a pair
of Ts". You don't write the type when you call it: the compiler infers T
from the arguments, so T is i32 in the first call and &str in the
second. Both elements have to be the same T, so swap_pair((1, "one"))
doesn't compile. A function can have several type parameters when they may
differ:
fn tag<K, V>(key: K, value: V) -> (K, V) {
(key, value)
}
fn main() {
let entry = tag("port", 8080);
println!("{} = {}", entry.0, entry.1);
}port = 8080Generics cost nothing at runtime. The compiler generates a separate copy of
the function for every type it is used with (swap_pair for i32, for
&str and for bool), a process called monomorphization. The result is
as fast as if you had written each version by hand.
Generic structs and enums
Structs and enums take type parameters the same way, after their name. Each field can use the parameter as its type:
#[derive(Debug)]
struct Range<T> {
low: T,
high: T,
}
#[derive(Debug)]
enum Reading<T> {
Value(T),
Missing,
}
fn main() {
let ages = Range { low: 18, high: 65 };
let prices = Range {
low: 9.99,
high: 24.5,
};
let letters = Range {
low: 'a',
high: 'f',
};
println!("{ages:?}");
println!("{prices:?}");
println!("{} to {}", letters.low, letters.high);
let readings = [Reading::Value(21.5), Reading::Missing];
println!("{readings:?}");
}Range { low: 18, high: 65 }
Range { low: 9.99, high: 24.5 }
a to f
[Value(21.5), Missing]Range<i32>, Range<f64> and Range<char> are three different types built
from one definition. Reading<T> has exactly the shape of the standard
Option<T>, which is defined as enum Option<T> { None, Some(T) }, and
Result<T, E> is an enum with two type parameters.
Methods on generic types
To write methods for a generic struct, declare the parameter on the impl
too: impl<T> Stack<T> reads "for any T, here are the methods of
Stack<T>":
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn size(&self) -> usize {
self.items.len()
}
}
fn main() {
let mut plates = Stack::new();
plates.push("blue");
plates.push("red");
println!("{} plates", plates.size());
println!("{:?}", plates.pop());
let mut numbers: Stack<u64> = Stack::new();
numbers.push(7);
println!("{:?} {:?}", numbers.pop(), numbers.pop());
}2 plates
Some("red")
Some(7) NoneThe <T> after impl declares the parameter; the <T> in Stack<T> uses
it. Inside the block, T is whatever type the stack holds. Stack::new()
doesn't mention the type: the compiler works it out from the first push, or
from an annotation like Stack<u64> when nothing else tells it.
You can also write methods for one concrete type only. impl Stack<f64>
adds methods that exist on stacks of f64 and nowhere else:
struct Stack<T> {
items: Vec<T>,
}
impl Stack<f64> {
fn total(&self) -> f64 {
let mut sum = 0.0;
for item in &self.items {
sum += item;
}
sum
}
}
fn main() {
let weights = Stack {
items: vec![1.5, 2.25, 0.25],
};
println!("{}", weights.total());
}4A Stack<&str> has no total method at all.
Trait bounds
Try to compare two values of a generic type and the compiler stops you:
fn is_between<T>(value: T, low: T, high: T) -> bool {
value >= low && value <= high
}
fn main() {
println!("{}", is_between(7, 1, 10));
}T could be any type at all, including types that can't be compared, so >=
isn't allowed. A trait bound narrows T down to the types that implement
a trait, and in return lets you use that trait's methods and operators. Write
it after the parameter with a colon, T: PartialOrd, just like the compiler
suggests:
fn is_between<T: PartialOrd>(value: T, low: T, high: T) -> bool {
value >= low && value <= high
}
fn main() {
println!("{}", is_between(7, 1, 10));
println!("{}", is_between(0.5, 1.0, 2.0));
println!("{}", is_between("kiwi", "apple", "mango"));
}true
false
trueIntegers, floats and string slices all implement PartialOrd, so they can all
be used. Calling is_between with a type that doesn't implement it is a compile
error at the call site.
Bounds work the same way with your own traits. With the Shape trait from the
traits lesson, T: Shape accepts any shape and lets the function call area:
trait Shape {
fn area(&self) -> f64;
}
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn total_area<T: Shape>(shapes: &[T]) -> f64 {
let mut sum = 0.0;
for shape in shapes {
sum += shape.area();
}
sum
}
fn main() {
let tiles = [Square { side: 1.0 }, Square { side: 2.0 }];
println!("{}", total_area(&tiles));
}5In the traits lesson you wrote shape: &impl Shape. That is shorthand for
exactly this: a generic parameter bounded by Shape.
Bounds also go on impl blocks. Methods in impl<T: Display> Stack<T> only
exist on stacks whose items can be displayed, while the methods in a plain
impl<T> block exist on every stack.
Multiple bounds and where clauses
Join several bounds with +. The type must implement all of them:
use std::fmt::Display;
fn announce_twice<T: Display + Clone>(item: T) -> (T, T) {
println!("got {item}");
(item.clone(), item)
}
fn main() {
let copies = announce_twice(String::from("ticket"));
println!("{} {}", copies.0, copies.1);
}got ticket
ticket ticketDisplay makes {item} work, and Clone makes .clone() work. Remove
either bound and the line that needs it stops compiling.
When there are several parameters with several bounds each, the signature
gets hard to read. A where clause moves the bounds after the return
type, one line per parameter:
use std::fmt::Debug;
use std::fmt::Display;
fn report<K, V>(key: K, values: &[V]) -> String
where
K: Display,
V: Debug + PartialEq,
{
let first = &values[0];
let mut same = true;
for value in values {
if value != first {
same = false;
}
}
format!("{key}: {values:?}, all equal: {same}")
}
fn main() {
println!("{}", report("flags", &[true, true, true]));
println!("{}", report(7, &['a', 'b']));
}flags: [true, true, true], all equal: true
7: ['a', 'b'], all equal: falseBoth forms mean exactly the same thing; where is just easier to read once
the bounds grow. rustfmt puts each bound on its own line, as above.
Converting with From and Into
From is the standard trait for converting one type into another. You have
already used it: String::from("hi") is From<&str> for String, and ?
uses From to convert errors. Implement From<T> for your type to say "my
type can be built from a T":
#[derive(Debug)]
struct Rgb {
red: u8,
green: u8,
blue: u8,
}
impl From<(u8, u8, u8)> for Rgb {
fn from(parts: (u8, u8, u8)) -> Self {
Rgb {
red: parts.0,
green: parts.1,
blue: parts.2,
}
}
}
impl From<u32> for Rgb {
fn from(hex: u32) -> Self {
Rgb {
red: (hex >> 16) as u8,
green: (hex >> 8) as u8,
blue: hex as u8,
}
}
}
fn main() {
let sky = Rgb::from((135, 206, 235));
let rust: Rgb = 0xB7410E.into();
println!("{sky:?}");
println!("{} {} {}", rust.red, rust.green, rust.blue);
}Rgb { red: 135, green: 206, blue: 235 }
183 65 14A type can implement From many times, once for each source type. And every
impl From<A> for B gives you the opposite direction for free: the standard
library implements Into<B> for A, so you can write a.into() wherever a B
is expected. into() needs to know the target type, which is why rust has
the annotation : Rgb. Implement From, never Into directly.
Into shines as a bound. A parameter of type impl Into<String> accepts a
&str, a String or anything else that converts into a String, and the
caller doesn't have to call .to_string() first:
struct Label {
text: String,
}
impl Label {
fn new(text: impl Into<String>) -> Self {
Label { text: text.into() }
}
}
fn main() {
let from_literal = Label::new("Save");
let owned = String::from("Cancel");
let from_string = Label::new(owned);
println!("{} {}", from_literal.text, from_string.text);
}Save CancelPassing a String moves it in without copying; passing a &str allocates a
new String, which the function needs anyway.
Borrowing with AsRef
Into converts and takes ownership. When a function only needs to look at the
data, AsRef<U> is the borrowing version: as_ref() turns a &T into a
&U, cheaply and without allocating. A bound like T: AsRef<[u32]> accepts
any type that can be viewed as a slice of u32s:
fn average<T: AsRef<[u32]>>(scores: T) -> f64 {
let scores = scores.as_ref();
let mut sum = 0;
for score in scores {
sum += score;
}
sum as f64 / scores.len() as f64
}
fn main() {
let week = vec![70, 85, 90];
println!("{:.1}", average(&week));
println!("{:.1}", average([60, 80]));
println!("{:.1}", average(week));
}81.7
70.0
81.7A Vec, a reference to one, and an array all work. You'll see AsRef all
over the standard library: std::fs::read_to_string takes
impl AsRef<Path>, which is why you can pass it a &str, a String or a
PathBuf. String and &str both implement AsRef<str>, so the same trick
lets a function accept either kind of string.
| Trait | Method | Use it when |
|---|---|---|
From<T> | U::from(t) | Implementing a conversion for your type |
Into<U> | t.into() | Accepting anything convertible, taking it |
AsRef<U> | t.as_ref() | Accepting anything viewable as &U, borrowed |
impl Trait in return position
impl Trait also works as a return type. It means "this function returns
some type that implements the trait, and the caller doesn't need to know
which":
use std::fmt::Display;
fn countdown(from: u32) -> impl Iterator<Item = u32> {
(1..=from).rev()
}
fn status(online: bool) -> impl Display {
if online { "online" } else { "offline" }
}
fn main() {
for n in countdown(3) {
println!("{n}");
}
println!("server is {}", status(true));
}3
2
1
server is onlineThe real return type of countdown is std::iter::Rev<RangeInclusive<u32>>.
Iterators get long, nested type names like that very quickly, and
impl Iterator<Item = u32> hides it behind what matters: "something you can
loop over that yields u32s". The caller can use every iterator method on
it, but nothing specific to Rev. The iterators lesson returns to this.
There is one rule: the function still returns one concrete type, chosen by
the compiler. impl Trait hides the type; it doesn't let different branches
return different types. That is what trait objects are for, in the next
lesson.
Common mistakes
Using a trait method without the bound
fn show_all<T>(items: &[T]) {
for item in items {
println!("- {item}");
}
}
fn main() {
show_all(&["eggs", "milk"]);
}Inside a generic function you can only use what the bounds promise. Add
T: std::fmt::Display (or T: Debug and print with {:?}). The compiler
checks the generic body once, against the bounds, not against the types you
happen to call it with.
Forgetting <T> after impl
struct Stack<T> {
items: Vec<T>,
}
impl Stack<T> {
fn size(&self) -> usize {
self.items.len()
}
}
fn main() {
let stack = Stack { items: vec![1, 2] };
println!("{}", stack.size());
}Without impl<T>, Rust looks for an actual type called T. Declare it:
impl<T> Stack<T>.
Calling into() without a target type
struct Celsius(f64);
impl From<f64> for Celsius {
fn from(degrees: f64) -> Self {
Celsius(degrees)
}
}
fn main() {
let reading = 21.5.into();
println!("{}", reading.0);
}A value can convert into many types, so into() needs to know which one you
mean. Annotate the variable (let reading: Celsius = 21.5.into();) or use
the other direction, Celsius::from(21.5), which names the target itself.
Returning different types from impl Trait
use std::fmt::Display;
fn score(points: u32, as_text: bool) -> impl Display {
if as_text { format!("{points} points") } else { points }
}
fn main() {
println!("{}", score(5, true));
}Both String and u32 implement Display, but impl Display still stands
for a single type. Convert both branches to the same type (here
points.to_string()), or return a trait object such as
Box<dyn Display>, covered in the next lesson.
Summary
- A type parameter in angle brackets,
fn name<T>,struct Name<T>orenum Name<T>, lets one definition work with many types. The compiler infersTand generates a copy per type, so generics cost nothing at runtime. - Methods on a generic type go in
impl<T> Name<T>;impl Name<f64>adds methods for one concrete type only. - A generic value can only do what its trait bounds allow. Add bounds with
T: Trait, combine them with+, and move long ones into awhereclause. &impl Traitin a parameter is shorthand for a bounded generic.- Implement
Fromto convert into your type;Intocomes for free.impl Into<String>parameters accept both&strandString. AsRef<U>borrows a value as a&U, so one function can take aVec, an array or a slice, or aStringor a&str.impl Traitas a return type hides a long concrete type, but the function still returns exactly one type.
Practise what you read
Challenges that use the ideas from this lesson.