Skip to content

Generics

A generic function or type works for many types; trait bounds say what those types must be able to do, and the compiler makes a separate copy for each type used.

Java generics are checked at compile time and then erased. At run time, List<String> and List<Integer> are the same class, and the type argument is gone:

Java

static <T extends Comparable<T>> Optional<T> largest(List<T> items) {
T best = null;
for (T item : items) {
if (best == null || item.compareTo(best) > 0) {
best = item;
}
}
return Optional.ofNullable(best);
}

Rust

/// The largest item, or `None` for an empty slice. Works for any `T` that can be compared.
pub fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
let mut iter = items.iter();
let mut best = iter.next()?;
for item in iter {
if item > best {
best = item;
}
}
Some(best)
}

The two functions look alike. <T extends Comparable<T>> in Java and <T: PartialOrd> in Rust both say “T must be comparable”. The difference is in what the compiler does with them.

Rust makes a separate copy of a generic function for each concrete type that uses it. largest::<i32> and largest::<&str> are two functions in the binary, each compiled and optimized for its type. This is called monomorphization.

The consequences, compared to Java:

Java (erasure) Rust (monomorphization)
Speed Calls go through interfaces, values are boxed (Integer) Direct calls, no boxing: as fast as hand-written code
Binary size One copy One copy per type: larger binaries, longer compile times
Primitives Not allowed (List<int> does not exist) Allowed (Vec<u8>, largest::<f64>)
Type at run time Erased: no instanceof List<String> Known: each copy has its own type
/// The largest item, or `None` for an empty slice. Works for any `T` that can be compared.
pub fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
let mut iter = items.iter();
let mut best = iter.next()?;
for item in iter {
if item > best {
best = item;
}
}
Some(best)
}

There is no null for “nothing found”: the function returns Option<&T>, and ? on line 4 returns None for an empty slice.

A bound says which traits a type parameter must implement. Without a bound, a function can do almost nothing with a T: not compare it, not print it, not copy it. You will see the same bounds written in three ways:

/// Three ways to write the same bounds.
pub fn report_a<T: Display + Clone>(items: &[T]) -> String {
join(items)
}
pub fn report_b<T>(items: &[T]) -> String
where
T: Display + Clone,
{
join(items)
}
pub fn report_c(items: &[impl Display + Clone]) -> String {
join(items)
}
  • <T: Display + Clone>: inline. Good for short bounds.
  • where T: Display + Clone: a where clause. Good for long bounds, and for bounds that are not on a plain parameter.
  • impl Display + Clone in argument position: short, but the caller cannot name the type with a turbofish (report_c::<u32> does not work).

All three compile to the same code.

A struct or an enum can have type parameters. The bounds usually go on the impl block, not on the struct, so that the struct can be named without them:

/// Counts how often each key occurs. `K` is chosen by the caller.
#[derive(Debug)]
pub struct Counter<K> {
counts: HashMap<K, usize>,
}
impl<K: Eq + Hash> Counter<K> {
pub fn new() -> Self {
Self {
counts: HashMap::new(),
}
}
pub fn add(&mut self, key: K) {
*self.counts.entry(key).or_insert(0) += 1;
}
pub fn get(&self, key: &K) -> usize {
self.counts.get(key).copied().unwrap_or(0)
}
}
  1. Line 3: Counter<K>: a type parameter with no bounds. Option<T>, Vec<T> and HashMap<K, V> are defined the same way.
  2. Line 7: impl<K: Eq + Hash> Counter<K>: these methods exist only when K can be a hash map key. Read it as "for every K that is Eq and Hash".
  3. Line 14: add(&mut self, key: K) takes the key by value, because the map stores it.
  4. Line 18: get(&self, key: &K) only borrows the key, because it only looks it up.

Some traits have a type that each implementation chooses once, for example Iterator and its Item. This is an associated type:

/// An iterator over order IDs. `Item` is an associated type: each iterator type has one item type.
pub struct OrderIds {
next: u64,
last: u64,
}
impl Iterator for OrderIds {
type Item = u64;
fn next(&mut self) -> Option<u64> {
if self.next > self.last {
return None;
}
self.next += 1;
Some(self.next - 1)
}
}
pub fn ids(first: u64, last: u64) -> impl Iterator<Item = u64> {
OrderIds { next: first, last }
}

impl Iterator<Item = u64> in the return type means “some iterator type that yields u64, which the caller does not need to name”. A generic parameter (trait Iterator<T>) would let one type implement the trait many times, with a different T each time. An associated type allows one implementation per type, which is what an iterator needs. When you read Item = u64, read it as “the iterator’s element type is u64”.

Sometimes a type has a type parameter that none of its fields store. PhantomData<T> is a zero-size marker that “uses” T, so that the compiler accepts the parameter:

/// An ID that is typed by the entity it belongs to. `PhantomData<T>` uses `T`
/// without storing a `T`, so `Id<Customer>` and `Id<Order>` are different types.
pub struct Id<T> {
pub value: u64,
entity: PhantomData<T>,
}
impl<T> Id<T> {
pub fn new(value: u64) -> Self {
Self {
value,
entity: PhantomData,
}
}
}

Id<Customer> and Id<Order> are now different types, but both are only a u64 in memory. You need to recognize PhantomData, not write it. When you see it, ask what the type parameter protects.

  • Generic only where more than one type is used. A type parameter that is always u64 is noise.
  • The smallest bounds that the body needs. If the body only compares, the bound is PartialOrd, not Clone + Debug + Send + Sync.
  • Bounds on impl blocks and functions, not on struct definitions.
  • impl Trait in argument position for simple cases, named type parameters when the caller must choose the type, or when two parameters must have the same type.
  • Concrete types in application code. Generic code is common in libraries. In an application, most functions take concrete types.
  • A type parameter that is used with one type only. fn shipping<C: Into<u64>>(country: C), called only with u16. The generic adds compile time and reading time for nothing.
  • Very long bounds “to be safe”: T: Clone + Debug + Send + Sync + 'static on a function that only adds numbers. Each bound limits the callers. Ask which bounds the body needs.
  • Very long where clauses that repeat in many functions. A helper trait, or a simpler design, is often better.
  • Box<dyn Trait> where a generic works. Dynamic dispatch and a heap allocation for one type per call. See S22: Box<dyn Trait> where a generic or enum is sufficient.
  • Generics to imitate Java’s Object: T: Any with downcasts. The code needs an enum or a trait with methods.

The task: sum the lines of an order, and compute shipping by country code.

  1. WorseTwo type parameters with six bounds, most of them unused, and a generic country_code that is always a u16.

    pub fn total_cents<T, I>(lines: I) -> u64
    where
    T: Into<u64> + Clone + Debug + Send + Sync + 'static,
    I: IntoIterator<Item = T> + Clone + Debug,
    {
    lines.into_iter().map(Into::into).sum()
    }
    pub fn shipping<C: Into<u64>>(country_code: C) -> u64 {
    if country_code.into() == 41 {
    900
    } else {
    1_500
    }
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterOne impl IntoIterator<Item = u64> parameter, and a plain u16.

    pub fn total_cents(lines: impl IntoIterator<Item = u64>) -> u64 {
    lines.into_iter().sum()
    }
    pub fn shipping(country_code: u16) -> u64 {
    if country_code == 41 { 900 } else { 1_500 }
    }
    Run in Playground (opens the Rust Playground in a new tab)

The “worse” total_cents compiles and works. But Clone + Debug + Send + Sync + 'static on the element type rejects callers whose types do not have those traits, and the body uses none of them. I: Clone + Debug on the iterator is also unused. Error messages for a wrong call list all six bounds, which makes them hard to read.

The “worse” shipping makes every call generic for a parameter that is always a country code. A plain u16 says what the value is.

Generic code is correct when callers use different types: a library function that works on any IntoIterator, a container like Counter<K>, or a function that accepts &str, String and &String with impl AsRef<str>.

  1. What is the main difference between Java generics and Rust generics at run time?

    Show the answer

    Java erases the type arguments, so there is one copy of the code. Rust makes a copy for each concrete type (monomorphization). Rust code is faster and can use primitive types directly. The cost is larger binaries and longer compile times.

  2. Why does fn largest<T>(items: &[T]) -> Option<&T> not compile if the body uses >?

    Show the answer

    Without a bound, the compiler knows nothing about T, so it cannot compare two values. The bound T: PartialOrd gives the > operator.

  3. What does impl Iterator<Item = u64> in a return type mean?

    Show the answer

    The function returns one specific iterator type that yields u64 values. The caller does not need to know the type’s name, but it is still static dispatch: no box and no vtable.

  4. A function has the bound T: Clone + Debug + Send + Sync + 'static, and only calls to_string(). What do you suggest?

    Show the answer

    Replace the bounds with T: Display (or ToString), which is all that the body needs. Fewer bounds accept more callers and give clearer error messages.