M Logo
๐Ÿ“ Rust for Beginners ยท Part 19 of 22

Rust Generics and Lifetimes Explained: Generic T, Trait Bounds and 'a

November 10, 2026

Welcome to episode 19 of Rust for Beginners, a series for complete beginners (no programming experience needed). In the last video, we learned about traits. Today, we'll learn generics: how to write code once, for many types. And at the end, a gentle first look at lifetimes. Every code block below is the exact code from the video, ready to copy.

1. New project

Let's create a new project called generics, and open it in VS Code.

bash
cargo new generics
cd generics

2. Generic fn

We want one function that finds the largest item in a list of numbers, and also in a list of letters. So instead of a real type, we use a placeholder, T, in angle brackets.

src/main.rs:

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

fn main() {
    let numbers = [34, 50, 25, 100, 65];
    let letters = ['y', 'm', 'a', 'q'];
    println!("Largest number: {}", largest(&numbers));
    println!("Largest letter: {}", largest(&letters));
}

Error:

text
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:4:17
  |
4 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

Stage A2 โ€“ line 1 becomes fn largest<T: PartialOrd>(list: &[T]) -> &T {

Output: Largest number: 100 / Largest letter: y

3. Error

Let's run it. Rust says the greater-than sign can't be used on T.

bash
cargo run

4. Bound

T could be any type, and not every type can be compared. The help tells us the fix: require the PartialOrd trait. That's called a trait bound.

5. Fix

Let's add colon PartialOrd after T. Now T can be any type that can be compared.

6. Run A

Let's run it. One function, two types.

bash
cargo run

7. Generic struct

Structs can be generic too. A Point of T has an x and a y of the same type T.

rust
struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let ints = Point { x: 5, y: 10 };
    let floats = Point { x: 1.5, y: 4.0 };
    let mixed = Point { x: 5, y: 4.0 };
    println!("{} {} {}", ints.x, floats.y, mixed.x);
}
text
error[E0308]: mismatched types
 --> src/main.rs:9:34
  |
9 |     let mixed = Point { x: 5, y: 4.0 };
  |                                  ^^^ expected integer, found floating-point number

8. Same T

So what about x five and y four point zero?

bash
cargo run

9. Why

x made T an integer, so y must be an integer too. One T means one type.

10. impl<T>

Let's remove the mixed point. Methods on a generic struct use impl of T. Then Points of integers and Points of floats both get the method.

bash
cargo run
rust
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let ints = Point { x: 5, y: 10 };
    let floats = Point { x: 1.5, y: 4.0 };
    println!("ints: {}, {}", ints.x(), ints.y);
    println!("floats: {}, {}", floats.x(), floats.y);
}

Output: ints: 5, 10 / floats: 1.5, 4

11. Lifetimes

Now, lifetimes. longest returns the longer of two string slices. Let's run it.

bash
cargo run
rust
fn longest(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let first = String::from("Rust");
    let second = String::from("Ferris");
    println!("Longest: {}", longest(&first, &second));
}
text
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

Stage C2 โ€“ line 1 becomes fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {

Output: Longest: Ferris

12. The question

Rust asks: the result is borrowed, but from a or from b? It needs to know how long the result stays valid.

13. 'a

We add a lifetime parameter, tick a. It says: the result lives as long as both inputs. We don't change how long anything lives. We just describe it, so Rust can check.

14. Run C

Let's run it. Ferris is longer.

bash
cargo run

15. Check

Now the check in action: second dies at the end of the inner block, but we use result after it.

bash
cargo run
rust
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let first = String::from("Rust");
    let result;
    {
        let second = String::from("Ferris");
        result = longest(&first, &second);
    }
    println!("Longest: {result}");
}
text
error[E0597]: `second` does not live long enough
  --> src/main.rs:10:34
   |
 9 |         let second = String::from("Ferris");
   |             ------ binding `second` declared here
10 |         result = longest(&first, &second);
   |                                  ^^^^^^^ borrowed value does not live long enough
11 |     }
   |     - `second` dropped here while still borrowed
12 |     println!("Longest: {result}");
   |                         ------ borrow later used here

16. Error

second does not live long enough. Rust caught a dangling reference before the program ever ran.

Recap

To recap: generics like T let one function or struct work with many types. Trait bounds say what T must be able to do. And lifetimes like tick a tell Rust how long references stay valid.

ยฉ 2026 All Rights Reserved By Mahmudul Alam