Generics

Course 2 · Ch 3
Generics
Writing code once, over many types — and why you've already been using generics since Course 1

Chapter 2 introduced trait bounds as a way to constrain a generic parameter. This chapter is fully about generics themselves — writing logic once instead of duplicating it per type, with trait bounds as the way to say what that type must be able to do.

The Problem Generics Solve

Without generics, comparing the largest value in a list would need a separate, identical function per type — largest_i32, largest_f64, largest_char. Generics let that logic exist once:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest }

The trait bounds aren't decoration — they're required by what the function's body actually does: PartialOrd because > needs to be defined for T; Copy because a value can't be moved out of a slice reference, only copied.

Generic Structs

struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } }

Both fields share type T here. When fields genuinely need to differ, use multiple type parameters: struct Point<T, U> { x: T, y: U }.

Generic Enums: You've Already Been Using These

Course 1's Option<T> (Chapter 6) and Result<T, E> (Chapter 7) are themselves generic enums — you've been using generics since Chapter 6, just without the formal vocabulary for it until now.

Monomorphization: How Generics Compile

At compile time, Rust generates a separate, fully concrete version of a generic function or struct for every distinct type it's actually used with — calling largest with a slice of i32 and again with a slice of f64 produces two entirely separate compiled functions internally. This means generics carry zero runtime cost — no dynamic dispatch, no boxing — the exact same "real analysis, zero runtime cost" story Course 1 told about the borrow checker and lifetimes, now extended to generics.

Contrast With Go's Generics (go3-1)

Go added generics far later than Rust (Go 1.18, 2022), using comparable constraint syntax ([T any]). The real engineering difference: Rust always fully monomorphizes every generic instantiation. Go's compiler, to control binary size and compile time, has used a mix of monomorphization and shared/dictionary-based code generation for certain cases — meaning not every Go generic instantiation gets its own fully specialized compiled copy the way Rust's always does. This is a genuine, deliberate trade-off both languages made — Go leaning toward smaller binaries and faster compiles in some cases, Rust always prioritizing runtime performance — not a case of one being straightforwardly better.

GoRust
Generics added2022 (Go 1.18)Present since Rust 1.0
Compilation strategyMix of monomorphization and shared/dictionary codeAlways fully monomorphized
Optimizes forBinary size / compile speed in some casesRuntime performance, always

Generic Function

One implementation, parameterized over T, with trait bounds specifying what T must support.

Generic Struct

Fields typed by one or more type parameters, e.g. Point<T> or Point<T, U>.

Option<T> / Result<T, E>

Generic enums you've already used since Course 1 — now named and understood properly.

Monomorphization

A separate compiled version generated per concrete type — zero runtime cost, larger binary.

You Already Know Generics
Every time Some(value), None, Ok(value), or Err(error) appeared back in Course 1, that was a generic type already at work. This chapter didn't introduce a new concept from scratch — it gave a name and a formal mechanism to something already familiar.
Forgetting the Bound an Operation Actually Needs
fn largest<T>(list: &[T]) -> T with a body that uses > internally, but no T: PartialOrd bound, produces a compile error saying > can't be applied to type T. The function looks like it should "just work for any T" — but the compiler has no way to know that unless the bound says so explicitly. Whatever an operation inside a generic function actually needs (comparison, cloning, formatting), the corresponding trait has to be named in the bound.

Coding Challenges

Challenge 1

Write a generic function smallest(list: &[T]) -> T, following this chapter's largest as a template, and call it with both a slice of i32 and a slice of char.

📄 View solution
Challenge 2

Define a generic struct Pair with fields first: T and second: U, and an impl block with a method describe(&self) -> String requiring T: std::fmt::Display and U: std::fmt::Display. Explain why the bound has to go on the impl block (or the method) rather than the struct definition itself.

📄 View solution
Challenge 3

Explain what "zero runtime cost" actually means for monomorphized generics, referencing what the compiler produces for two different calls to the same generic function with different types — and contrast this briefly with what a dynamic-dispatch-based alternative would cost at runtime instead.

📄 View solution

Chapter 3 Quick Reference

  • fn f<T: Bound>(x: T) — one implementation over many types, constrained by what the body actually needs
  • struct Name<T> { field: T } — a generic struct; use multiple parameters (T, U) when fields differ in type
  • Option<T> and Result<T, E> are themselves generic enums, in use since Course 1
  • Monomorphization — the compiler generates a fully separate version per concrete type used; zero runtime cost, larger binary
  • Go's generics (added 2022) sometimes use shared/dictionary code instead of always monomorphizing — a deliberate binary-size/compile-speed trade-off, not a worse design
  • A generic function's trait bounds must name whatever the body's operations actually require — the compiler won't infer this from "it should just work"
  • Next chapter: Smart Pointers — Box<T>, Rc<T>, RefCell<T>, and interior mutability