Skip to Content
Course content

35: Generic Functions and Structs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost every developer I've mentored when they first hit generics. You want to write a utility function that works for any number—integers, floats, you name it—so you decide to stop repeating yourself and use a generic type T. It feels right until you try to actually do something with that data.

The 'T' that can't be compared

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

If you try to compile this, Rust is going to scream at you. The error will be something along the lines of binary operation `>` cannot be applied to type `T`. It's a bit frustrating at first because, in your head, T is obviously a number. But to the compiler, T is anything. It could be a String, a custom User struct, or a TcpStream. Not everything in the universe can be compared with a "greater than" operator.

Giving Rust a hint with Trait Bounds

To fix this, we have to tell Rust that T isn't just any type—it's a type that specifically knows how to be compared. In Rust, we do this using trait bounds. For comparison, we use the PartialOrd trait.

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

Notice the <T: PartialOrd> syntax. I'm essentially telling the compiler: "This function works for any type T, as long as T implements the PartialOrd trait." Now the compiler is happy because it has a guarantee that the > operator actually exists for whatever type the caller passes in.

Structuring data for any type

Generics aren't just for functions; they're incredibly powerful for structs. Imagine you're building a system to track a "current value" and a "previous value" for some kind of sensor. You wouldn't want to write one struct for i32, one for f64, and another for u16.

Instead, you define the struct with a generic type parameter. I usually do this when I know the structure of the data is constant, but the specific type of the data might change depending on the use case.

struct History<T> {
    current: T,
    previous: Option<T>,
}

impl<T> History<T> {
    fn update(&mut self, new_value: T) {
        self.previous = Some(std::mem::replace(&mut self.current, new_value));
    }
}

One thing to keep in mind: when you implement methods for a generic struct, you have to declare the generic type again after the impl keyword. It looks redundant—impl<T> History<T>—but you're telling Rust that the implementation is generic over T, not for one specific version of History.

I personally find that generics make code significantly more maintainable. Instead of having five different versions of a "wrapper" or "container," you have one source of truth. Just remember: the moment you need to perform an action on that generic type (like adding, comparing, or printing it), you'll need to reach for those trait bounds.




📋 Practical Task

Implementing a Generic Result Cache

You are building a caching system that stores the result of an expensive computation. You need to create a generic struct called CacheEntry<T> that stores a value of type T and a timestamp (represented as a u64) of when it was created.

Your task is to:

  • Define the CacheEntry<T> struct.
  • Implement a method is_expired(&self, current_time: u64, ttl: u64) -> bool that returns true if the difference between current_time and the entry's timestamp is greater than the ttl (time-to-live).
  • Implement a method get_value(&self) -> &T that returns a reference to the cached value.
  • In your main function, instantiate one CacheEntry for a String and another for an i32, and verify that is_expired works for both.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.