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

Up until now, you've used generics to make your functions flexible. But you've probably already hit a wall where the compiler tells you that it doesn't know if a generic type T actually supports the operation you're trying to perform. That's where trait bounds come in. They're essentially how you tell Rust, "I don't care exactly what type this is, as long as it knows how to do X, Y, and Z."

Why can't I just use a generic T without any constraints?

If you write a function fn print_it<T>(item: T), you've told Rust that T can be anything. Literally anything. It could be an integer, a string, or a complex custom struct you wrote for a database connection. Because of that, Rust won't let you do anything with item that isn't universal to every single type in existence.

For example, if you try to print that item using println!("{:?}", item), the compiler will stop you. Why? Because not every type implements the Debug trait. To fix this, you add a trait bound. You're basically putting a requirement on the caller of the function.

use std::fmt::Debug;

// The ": Debug" part is the trait bound
fn log_value<T: Debug>(value: T) {
    println!("The current value is: {:?}", value);
}

fn main() {
    log_value(42);            // Works! i32 implements Debug
    log_value("Hello Rust");  // Works! &str implements Debug
    // log_value(SomeCustomStructWithoutDebug); // This would fail to compile
}

How do I require a type to implement multiple traits?

In the real world, you often need a type to do more than one thing. Maybe you need a type that can be compared (so you can find the maximum) and also be printed (so you can log the result). You can chain these requirements using the + symbol.

I've found that this is where things start to get powerful. You aren't just restricting the type; you're defining a "capability profile" for your function. Check out this example where we want to find the "best" item in a list and print it.

use std::fmt::Display;

fn print_max<T: PartialOrd + Display>(list: &[T]) {
    if let Some(max) = list.iter().max_by(|a, b| a.partial_cmp(b).unwrap()) {
        println!("The highest value is: {}", max);
    }
}

fn main() {
    let numbers = vec![10, 50, 20];
    print_max(&numbers); // Works: i32 is PartialOrd + Display

    let words = vec!["apple", "zebra", "banana"];
    print_max(&words); // Works: &str is PartialOrd + Display
}

When should I use a "where" clause instead of the angle brackets?

When you start adding multiple bounds to multiple generic types, your function signature starts to look like a mess of angle brackets. It becomes hard to actually see the function name and the arguments. This is why Rust provides the where clause. It doesn't change how the code works—it's purely for readability.

I generally switch to where clauses as soon as I have more than one generic type or more than two bounds per type. It separates the "what this function takes" from the "what these types must be able to do."

use std::fmt::Display;

// This is getting crowded and hard to read:
fn compare_and_print<T: PartialOrd + Display, U: PartialOrd + Display>(a: T, b: U) {
    // ... logic ...
}

// This is much cleaner:
fn compare_and_print_clean<T, U>(a: T, b: U) 
where 
    T: PartialOrd + Display, 
    U: PartialOrd + Display 
{
    // The logic stays the same, but the signature is breathable
    println!("Comparing values...");
}



📋 Practical Task

Exercise: Building a Generic High-Score Tracker

You are building a gaming leaderboard. You need to create a function called get_top_score that takes a slice of scores and returns the highest one. However, since scores could be integers (points) or floats (timed runs), the function must be generic.

Requirements:

  • Implement a function get_top_score<T>(scores: &[T]) -> Option<T>.
  • The function must use trait bounds to ensure T implements PartialOrd (so you can compare them) and Copy (so you can return the value from the slice).
  • Use a where clause to define these bounds to keep the signature clean.
  • In your main function, test it with a Vec<i32> and a Vec<f64>.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.