Rust Generics and Trait Bounds: From T to where, Slices, and AsRef

Author

Andres Monge

Published

June 15, 2026

Rust generics are easiest to understand if you think of them as placeholders for types.

Instead of writing one function for i32, another for f64, and another for String, you can write one function that works with some type T.

pub fn hola<T>(a: T) {
    todo!()
}

Here, T means:

This function accepts a value of some type, but I am not naming the concrete type yet.

That is powerful, but there is an important question:

What is this function allowed to do with T?

By default, not much.

If you want to compare, print, copy, clone, or order generic values, you need trait bounds.

Generic Type Parameters

This function accepts one value of any type:

pub fn hola<T>(a: T) {
    todo!()
}

The type parameter appears in two places:

fn hola<T>(a: T)

Read it as:

hola is generic over T, and a has type T.

The caller chooses the concrete type when calling the function.

hola(10);
hola("hello");
hola(String::from("Andres"));

But inside the function, Rust cannot assume T supports every operation.

For example, this is not automatically allowed:

pub fn bigger<T>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

The problem is that not every type can be compared with >.

Rust needs you to say that comparison is required.

Trait Bounds Say What T Must Support

A trait bound adds a requirement to a generic type.

pub fn bigger<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

This means:

T can be any type, as long as values of T can be partially ordered.

The bound is the important part:

T: PartialOrd

Without that bound, Rust does not know whether > is valid.

With that bound, Rust knows the function may use comparison operators like <, >, <=, and >=.

Comparison Traits

Rust separates comparison behavior into traits.

PartialEq

PartialEq gives equality and inequality:

==
!=

Example:

pub fn same<T: PartialEq>(a: T, b: T) -> bool {
    a == b
}

Use PartialEq when you only need to know whether two values are equal or different.

PartialOrd

PartialOrd gives partial ordering:

<
>
<=
>=

Example:

pub fn is_before<T: PartialOrd>(a: T, b: T) -> bool {
    a < b
}

It is called partial ordering because some values may not have a clean ordering relation.

Floating-point numbers are the classic example because of NaN.

let value = f32::NAN;

assert_eq!(value < 1.0, false);
assert_eq!(value > 1.0, false);
assert_eq!(value == value, false);

That is why f32 and f64 implement PartialOrd, but not Ord.

Ord

Ord means total ordering.

With total ordering, any two values can be consistently compared and sorted.

Types like integers implement Ord:

pub fn max_value<T: Ord>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

Use Ord when your function needs a complete ordering, such as sorting keys or choosing a maximum from values that always have a clear order.

Use PartialOrd when you want to support more types, including floating-point numbers.

Multiple Trait Bounds

Sometimes one requirement is not enough.

For example, this function wants to compare values and also print them with debug output.

use std::fmt::Debug;

pub fn find<T: PartialOrd + Debug>(array: &[T], key: T) {
    todo!()
}

The + means both bounds are required:

T: PartialOrd + Debug

Read it as:

T must support partial comparison and debug formatting.

This is useful when the function needs several behaviors from the same generic type.

where Clauses

When bounds get longer, putting everything inside the angle brackets can become noisy.

This works:

pub fn find<T: PartialOrd + Copy>(array: &[T], key: T) -> Option<usize> {
    todo!()
}

But many Rust programmers prefer a where clause once the bounds become more complex:

pub fn find<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialOrd + Copy,
{
    todo!()
}

This means the same thing.

The where clause just moves the requirements below the function signature.

Use a where clause when it makes the function easier to read.

Why Copy Appears in Simple Examples

Here is a small find function:

pub fn find<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialOrd + Copy,
{
    for (index, item) in array.iter().enumerate() {
        if *item == key {
            return Some(index);
        }
    }

    None
}

This example uses Copy because item is a reference from the slice:

item: &T

So *item tries to copy the T value out for comparison.

That is fine for small copyable values like integers.

let numbers = [10, 20, 30];

assert_eq!(find(&numbers, 20), Some(1));

But Copy is not always the best bound for real APIs because many useful types, like String, are not Copy.

One common alternative is to compare references instead.

pub fn find<T>(array: &[T], key: &T) -> Option<usize>
where
    T: PartialEq,
{
    for (index, item) in array.iter().enumerate() {
        if item == key {
            return Some(index);
        }
    }

    None
}

Now the function borrows the key instead of taking ownership of it.

let names = [String::from("Ana"), String::from("Bob")];
let key = String::from("Bob");

assert_eq!(find(&names, &key), Some(1));

The right bound depends on what the function needs to do.

Multiple Generic Types

A function can have more than one generic type parameter.

pub fn find<T, H>(array: &[T], key: &H) -> Option<usize>
where
    T: PartialOrd<H>,
{
    todo!()
}

Here there are two type parameters:

T
H

Read the signature as:

The array contains values of type T, and the key is borrowed as type H.

The bound says:

T: PartialOrd<H>

That means:

A T value can be compared with an H value.

This is a cross-type comparison.

Cross-Type Comparisons

Most beginner examples compare one type with itself:

T: PartialOrd

That is shorthand for comparing T with T.

But PartialOrd can also take another type parameter:

T: PartialOrd<H>

This means values of type T know how to compare themselves with values of type H.

For example, you might want the values in a collection and the search key to have different but comparable types.

The main idea is:

The bound describes the comparison you want to perform.

If your code compares item with key, the trait bound must say that this comparison is valid.

Rust Does Not Support Function Overloading

Some languages let you define multiple functions with the same name as long as the argument types are different.

Rust does not support that kind of function overloading.

This is not allowed:

pub fn find<T>(array: &[T], key: T) {}

pub fn find<T>(array: [T; 3], key: T) {}

Both functions are named find, so Rust rejects the duplicate definition.

Instead, common Rust style is to design one function signature that accepts the shape you really want.

For collection-like inputs, that often means accepting a slice.

Arrays, Slices, and Unsized [T]

Rust has fixed-size arrays:

[T; N]

For example:

let numbers: [i32; 3] = [10, 20, 30];

The 3 is part of the type.

So these are different array types:

[i32; 3]
[i32; 4]

A slice is different:

&[T]

A slice is a borrowed view into a sequence of T values.

pub fn print_all<T: std::fmt::Debug>(items: &[T]) {
    for item in items {
        println!("{item:?}");
    }
}

This can accept a borrowed view of many different array lengths.

let three = [1, 2, 3];
let four = [1, 2, 3, 4];

print_all(&three);
print_all(&four);

The type [T] by itself is unsized.

That means Rust does not know its size at compile time unless it is behind something like a reference.

So you usually see it as:

&[T]
Box<[T]>

For function parameters, &[T] is the common form.

Accepting Arrays as Slices

If a function accepts &[T], callers can pass arrays by reference.

pub fn find<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialEq + Copy,
{
    for (index, item) in array.iter().enumerate() {
        if *item == key {
            return Some(index);
        }
    }

    None
}

Usage:

let arr = [1, 2, 3];

assert_eq!(find(&arr, 2), Some(1));

The caller has an array.

The function receives a slice.

That is a normal Rust pattern.

Use &[T] when your function only needs to read a sequence and does not care about the exact array length.

AsRef<[T]>

Sometimes you want a public function to accept several input forms.

For example, callers might have:

  • an array
  • a slice
  • a vector
  • another type that can be viewed as a slice

AsRef<[T]> lets the function say:

Give me anything that can be borrowed as a slice of T.

pub fn find<T, A>(array: A, key: T) -> Option<usize>
where
    T: PartialEq + Copy,
    A: AsRef<[T]>,
{
    let array = array.as_ref();

    for (index, item) in array.iter().enumerate() {
        if *item == key {
            return Some(index);
        }
    }

    None
}

The important line is:

let array = array.as_ref();

After that, the function works with a plain slice:

&[T]

Usage:

let arr = [1, 2, 3];
let vec = vec![1, 2, 3];

assert_eq!(find(arr, 2), Some(1));
assert_eq!(find(&arr, 2), Some(1));
assert_eq!(find(vec, 2), Some(1));

This can make public APIs flexible.

But it can also make signatures more abstract.

So a good rule is:

Use &[T] by default. Use AsRef<[T]> when the extra input flexibility is worth the extra generic complexity.

Public Flexible API, Internal Simple Implementation

A nice pattern is to make the public function flexible and keep the internal function simple.

pub fn find<T, A>(array: A, key: T) -> Option<usize>
where
    T: PartialEq + Copy,
    A: AsRef<[T]>,
{
    find_slice(array.as_ref(), key)
}

fn find_slice<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialEq + Copy,
{
    for (index, item) in array.iter().enumerate() {
        if *item == key {
            return Some(index);
        }
    }

    None
}

The public function handles input flexibility:

A: AsRef<[T]>

The internal function handles the real work:

fn find_slice<T>(array: &[T], key: T) -> Option<usize>

This keeps the implementation easy to read.

The flexible part stays at the boundary.

The simple part stays inside.

Common Traits You Will See in Bounds

Trait bounds usually answer one question:

What behavior does this generic type need to support?

Here are common traits you will see in function signatures.

Debug

Debug allows developer-facing formatting with {:?}.

use std::fmt::Debug;

pub fn inspect<T: Debug>(value: T) {
    println!("{value:?}");
}

Use it when you want quick debugging output.

Display

Display allows user-facing formatting with {}.

use std::fmt::Display;

pub fn show<T: Display>(value: T) {
    println!("{value}");
}

Use it when the value should be printed in a clean human-readable form.

Clone

Clone allows explicit duplication.

pub fn duplicate<T: Clone>(value: T) -> (T, T) {
    (value.clone(), value)
}

Use it when copying may require real work, such as allocating a new String.

Copy

Copy allows cheap implicit copying.

pub fn pair<T: Copy>(value: T) -> (T, T) {
    (value, value)
}

Use it for small simple values like integers, booleans, characters, and references.

PartialEq

PartialEq allows == and !=.

pub fn contains<T: PartialEq>(items: &[T], key: &T) -> bool {
    items.iter().any(|item| item == key)
}

Use it when equality is enough.

Eq

Eq is a marker for full equality.

It builds on PartialEq and says equality behaves in the normal complete way.

Many key-like types implement Eq, such as integers and strings.

Floating-point numbers do not implement Eq because NaN breaks normal equality rules.

PartialOrd

PartialOrd allows <, >, <=, and >=.

Use it when values can be compared, but the ordering might not be total.

This is why it works for floats.

Ord

Ord allows total ordering.

Use it when every pair of values has a stable order.

This is common for sorting values or using ordered data structures.

Default

Default allows a type to create a default value.

pub fn make_default<T: Default>() -> T {
    T::default()
}

Use it when your function needs a starting value and the type knows what that should be.

AsRef<T>

AsRef<T> allows a value to be borrowed as another reference type.

pub fn length<A>(text: A) -> usize
where
    A: AsRef<str>,
{
    text.as_ref().len()
}

For this article, the important version is:

AsRef<[T]>

That means the input can be viewed as a slice of T.

Conversion traits like From and Into are also common, but they deserve their own article. They usually create or move values, while AsRef only borrows a view.

The AsRef<[T]> Family of Ideas

AsRef<[T]> can feel abstract at first because it has two layers:

AsRef<[T]>

The inner part is the target view:

[T]

The outer part is the ability:

AsRef<...>

Together, they mean:

This input can give me a borrowed slice view.

That is different from taking ownership of a collection.

The function does not need to know whether the caller started with an array, vector, or slice-like type.

It only needs this after calling as_ref():

&[T]

That makes AsRef<[T]> useful at API boundaries.

But inside the implementation, plain slices are usually easier to work with.

So this pattern is often the clearest:

pub fn public_function<T, A>(input: A)
where
    A: AsRef<[T]>,
{
    internal_function(input.as_ref())
}

fn internal_function<T>(input: &[T]) {
    todo!()
}

The public function says:

I accept flexible input forms.

The internal function says:

I just work with a slice.

That separation keeps the generic complexity from spreading through the whole program.

TL;DR: Purpose-Driven Cases

Use the bound based on what the generic code needs to do.

I only need any type

Use a plain generic type parameter.

pub fn accept<T>(value: T) {
    todo!()
}

I need equality

Use PartialEq.

pub fn same<T: PartialEq>(a: T, b: T) -> bool {
    a == b
}

I need ordering comparisons

Use PartialOrd or Ord.

pub fn before<T: PartialOrd>(a: T, b: T) -> bool {
    a < b
}

Use Ord only when the type must have a total ordering.

I need several behaviors

Use multiple bounds with +.

use std::fmt::Debug;

pub fn inspect_if_large<T: PartialOrd + Debug>(value: T, limit: T) {
    if value > limit {
        println!("{value:?}");
    }
}

The bounds are getting long

Use a where clause.

pub fn find<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialEq + Copy,
{
    todo!()
}

I want to accept a sequence without caring about array length

Use a slice.

pub fn find_slice<T>(array: &[T], key: T) -> Option<usize>
where
    T: PartialEq + Copy,
{
    todo!()
}

I want a flexible public API

Use AsRef<[T]> at the boundary.

pub fn find<T, A>(array: A, key: T) -> Option<usize>
where
    T: PartialEq + Copy,
    A: AsRef<[T]>,
{
    find_slice(array.as_ref(), key)
}

Conclusion

Generics let one function work with many concrete types.

Trait bounds say what those types must be able to do.

The key idea is:

T names a type placeholder. Bounds describe the behavior required from that type.

Use PartialEq for equality.

Use PartialOrd or Ord for ordering.

Use Debug, Display, Clone, Copy, and Default when the function needs those specific behaviors.

Use &[T] when your function only needs a borrowed sequence.

Use AsRef<[T]> when you want a public function to accept several sequence-like inputs.

And when an API starts to get flexible, keep the inside simple:

flexible public function, simple slice-based internal function.