SyntaxStudy
Sign Up
Rust Generic Bounds, where Clauses, and Associated Types
Rust Beginner 1 min read

Generic Bounds, where Clauses, and Associated Types

Trait bounds on generic parameters express the capabilities a type must have. Multiple bounds are combined with `+`: `T: Clone + Debug + PartialEq`. When bounds become long, the `where` clause moves them after the function signature for readability. Bounds can also apply to the associated types of a trait: `T: Iterator` constrains both the trait and the associated type simultaneously. Associated types are a form of type-level generics used in traits. Instead of the trait having a generic parameter (`trait Iterator`), the trait declares an associated type (`trait Iterator { type Item; }`). Implementors choose the concrete associated type once. This is preferred when a trait has exactly one sensible implementation per type — `Iterator::Item` is always exactly one type for a given iterator. The difference between a generic trait parameter and an associated type matters for type inference and disambiguation. With generic trait parameters, a type could implement the trait for many different parameters simultaneously (e.g. `From` and `From<&str>`). With associated types, there is only one implementation per type, which makes type inference more predictable and removes the need for explicit turbofish annotations in most contexts.
Example
use std::fmt::{Debug, Display};
use std::ops::Add;

// Multiple bounds with where clause
fn format_sum<T>(a: T, b: T) -> String
where
    T: Add<Output = T> + Display + Copy,
{
    let sum = a + b;
    format!("{a} + {b} = {sum}")
}

// Associated type in a custom trait
trait Transformer {
    type Input;
    type Output;
    fn transform(&self, input: Self::Input) -> Self::Output;
}

struct Doubler;
impl Transformer for Doubler {
    type Input = i32;
    type Output = i32;
    fn transform(&self, input: i32) -> i32 { input * 2 }
}

struct Stringify;
impl Transformer for Stringify {
    type Input = i32;
    type Output = String;
    fn transform(&self, input: i32) -> String { format!("#{input}") }
}

// Generic function constrained on the Transformer trait
fn apply<T: Transformer>(t: &T, input: T::Input) -> T::Output {
    t.transform(input)
}

// Bound on a generic struct
#[derive(Debug)]
struct DisplayVec<T: Display>(Vec<T>);

impl<T: Display> DisplayVec<T> {
    fn print_all(&self) {
        for (i, item) in self.0.iter().enumerate() {
            println!("  [{i}] {item}");
        }
    }
}

fn main() {
    println!("{}", format_sum(3, 4));
    println!("{}", format_sum(1.5_f64, 2.5));

    let d = Doubler;
    let s = Stringify;
    println!("doubled: {}", apply(&d, 21));
    println!("stringified: {}", apply(&s, 42));

    let dv = DisplayVec(vec!["alpha", "beta", "gamma"]);
    dv.print_all();
}