Generics

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and generics let you write code that works over many types without giving up any of that speed.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write generic functions and structs, add trait bounds, and see how Rust compiles them to specialized, zero-cost code.

What You'll Learn in This Lesson

1️⃣ Generic Functions and Trait Bounds

A generic function declares a type parameter in angle brackets, like <T>, then uses T in its signature. When the body needs an operation — comparing values, say — you add a trait bound like T: PartialOrd to require that capability.

// A generic function works for many types. <T> declares a type parameter.
// The bound  T: PartialOrd  means "T must be comparable with <, > etc."
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut biggest = &list[0];
    for item in list {
        if item > biggest {     // allowed because T: PartialOrd
            biggest = item;
        }
    }
    biggest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    println!("largest number: {}", largest(&numbers));   // -> 100

    let chars = vec!['y', 'm', 'a', 'q'];
    println!("largest char: {}", largest(&chars));       // -> y

    // The SAME function handled i32 and char with no duplication.
    let floats = vec![1.5, 9.2, 3.3];
    println!("largest float: {}", largest(&floats));     // -> 9.2
}

One function handled integers, chars, and floats. The bound T: PartialOrd is what makes the > comparison legal — it promises every T can be ordered.

2️⃣ Generic Structs and Methods

Structs can be generic too. Point<T> holds two values of the same type; Pair<T, U> lets the fields differ. Methods go in an impl<T> block, optionally with their own bounds — here T: Display so we can print the fields.

// Structs can be generic too. Point<T> holds two values of the SAME type T.
#[derive(Debug)]
struct Point<T> {
    x: T,
    y: T,
}

// impl<T> means the methods are available for every Point<T>.
impl<T: std::fmt::Display> Point<T> {
    fn show(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Two type parameters let the fields differ: Pair<T, U>.
#[derive(Debug)]
struct Pair<T, U> {
    first: T,
    second: U,
}

fn main() {
    let int_point = Point { x: 1, y: 2 };
    let float_point = Point { x: 1.5, y: 4.0 };
    int_point.show();      // -> (1, 2)
    float_point.show();    // -> (1.5, 4)

    let mixed = Pair { first: "age", second: 30 };
    println!("{:?}", mixed);   // -> Pair { first: "age", second: 30 }
}

This is exactly how the standard library is built: Vec<T>, Option<T>, and Result<T, E> are all generic types you've already been using.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — finish the generic function, then run it.

// This should return the FIRST element of any slice as a reference.
// 1) Declare the type parameter in the angle brackets.
fn first<___>(list: &[T]) -> &T {     // 👉 the type parameter name, T
    &list[0]
}

fn main() {
    let nums = vec![10, 20, 30];
    let words = vec!["a", "b", "c"];

    // 2) Call it on both — the same function handles different types.
    println!("first num: {}", first(&nums));    // expected: first num: 10
    println!("first word: {}", first(&___));    // 👉 the words vector
}

// ✅ Expected output:
//    first num: 10
//    first word: a

Mini-Challenge: A Generic Swapper

Write a generic function that swaps the two elements of a same-typed tuple. Run it with cargo run.

// 🎯 MINI-CHALLENGE: a generic pair swapper.
//
// 1. Write fn swap<T>(pair: (T, T)) -> (T, T) that returns the tuple with its
//    two elements swapped: (pair.1, pair.0).
// 2. In main, call swap((1, 2)) and print the result with {:?}.
// 3. Call swap(("left", "right")) and print that too — same function, new type.
//
// ✅ Example output:
//    (2, 1)
//    ("right", "left")

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Generics

GoalRust Syntax
Generic fnfn f<T>(x: T) { }
Trait boundfn f<T: PartialOrd>(x: T)
Generic structstruct P<T> { x: T }
Generic implimpl<T> P<T> { }
Two parametersstruct Pair<T, U>
where clausewhere T: Display

🎉 Lesson Complete!

Practice quiz

What main problem do generics solve?

  • Slower runtime
  • Memory leaks
  • Code duplication across types
  • Missing semicolons

Answer: Code duplication across types. Generics let one piece of code work over many types instead of duplicating it.

How do you declare a type parameter on a function?

  • fn f<T>(x: T)
  • fn f(T x)
  • fn f[T](x)
  • fn<T> f(x)

Answer: fn f<T>(x: T). A type parameter goes in angle brackets, like fn f<T>(x: T).

What is a trait bound like T: PartialOrd?

  • A runtime check
  • A default value
  • A lifetime
  • A requirement that T implements a trait

Answer: A requirement that T implements a trait. It constrains T to types implementing the trait, here enabling comparisons.

Which bound lets you compare values with the > operator?

  • T: Display
  • T: PartialOrd
  • T: Clone
  • T: Copy

Answer: T: PartialOrd. PartialOrd provides ordering comparisons like > and <.

Do generics slow your program down at runtime?

  • No, they are zero-cost
  • Yes, significantly
  • Only with floats
  • Only in debug builds

Answer: No, they are zero-cost. Monomorphization makes generics zero-cost at runtime.

What is monomorphization?

  • Boxing every value
  • Garbage collection
  • Generating a specialized copy per concrete type at compile time
  • Runtime type checking

Answer: Generating a specialized copy per concrete type at compile time. The compiler creates a fully optimized copy for each concrete type used.

How do you write a struct generic over two types?

  • struct Pair<T, T>
  • struct Pair<T, U>
  • struct Pair[T][U]
  • struct Pair(T, U)

Answer: struct Pair<T, U>. Two independent parameters are written struct Pair<T, U>.

Where must the type parameter be declared for a generic impl block?

  • After the type name
  • Inside the method
  • It is inferred
  • After the impl keyword, as impl<T>

Answer: After the impl keyword, as impl<T>. Write impl<T> Point<T>; the parameter is declared after impl.

Which bound is needed to print a generic value with {}?

  • T: PartialOrd
  • T: std::fmt::Display
  • T: Iterator
  • T: Send

Answer: T: std::fmt::Display. Printing with {} requires the Display trait bound.

Which standard-library types are generic?

  • Only Vec
  • Only String
  • Vec<T>, Option<T>, and Result<T, E>
  • None of them

Answer: Vec<T>, Option<T>, and Result<T, E>. Vec, Option, and Result are all generic types built the same way.

Continue this course

Frequently asked questions

What problem do generics solve?

Generics let you write a single piece of code that works for many different types, instead of duplicating it for each one. Without generics you might write largest_i32, largest_char, and largest_f64 separately. With a generic function largest<T>, you write the logic once and it works for any type that meets the requirements. This reduces duplication, keeps behaviour consistent, and makes code easier to maintain — the same idea powers Vec<T>, Option<T>, and Result<T, E> in the standard library.

What is a trait bound like T: PartialOrd?

A trait bound constrains a generic type parameter to types that implement a particular trait, which guarantees the operations you use are available. For example, comparing values with > requires the PartialOrd trait, so a function that compares its inputs must declare T: PartialOrd. Without the bound, the compiler would reject the comparison because not every possible type supports it. Trait bounds are how generics stay type-safe: you state what capabilities T must have, and the compiler enforces it.

Do generics make my program slower at runtime?

No. Rust uses a technique called monomorphization: at compile time it generates a specialized copy of your generic code for each concrete type that is actually used. So largest<i32> and largest<char> become two separate, fully optimized functions in the final binary. The result is that generic code runs exactly as fast as if you had written each version by hand — there is zero runtime cost for the abstraction. The trade-off is slightly larger compiled output and longer compile times.

Can a struct have more than one generic type parameter?

Yes. You can declare as many type parameters as you need, like struct Pair<T, U> { first: T, second: U }, which lets the two fields hold different types. Each parameter is independent and can have its own trait bounds. The standard library's Result<T, E> is a familiar example with two type parameters — one for the success value and one for the error. Use multiple parameters whenever the parts of your type genuinely vary independently.