Generics

Generics Syntax

Rust by example - Generics

Generics enable writing code that can work with different data types without having to write separate versions for each type. It is like creating a template, with placeholders that will be specified later.

Functions (fn), type aliases (type), structs (struct), enumerations (enum), unions, traits (trait), and implementations (impl) may be parameterized by types, constants, and lifetimes.

These parameters are listed in angle brackets (<...>), usually immediately after the name of the item and before its definition. For implementations, which don't have a name, they come directly after impl.

Use Type Parameters

The following example demonstrates generic type parameters, the most common. Instead of specifying concrete types (like i32, String, or bool) when defining functions, structs, enums, methods, etc., you can use a placeholder, typically denoted by an uppercase letter like T (for "Type"). This placeholder acts as a stand-in for any type that the user of your code might provide.

You will also encounter type parameters named K for a key, V for a value, F for a function or closure, S for a state, A for an allocator, and E for an error, among others. These are just conventions and can be replaced with any valid identifier.

use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::RandomState;

/// Generic function with a type parameter `T` (within < >).
///
/// Here, the type parameter is used as a placeholder for the type of the `t`
/// function argument.
fn print_type<T>(t: T) {
    println!("{}", std::any::type_name_of_val(&t));
}

/// - There may be multiple type parameters.
/// - Type parameters can be used to define the types of function arguments; or
///   the return type; or within the function.
/// - They may be used one or more times.
fn func<K, V, R>(key: K, value: V) -> R {
    let _k: K = key;
    let _v: V = value;
    unimplemented!();
}

/// Generic function with a type parameter that has a trait bound.
///
/// The `T: Debug` constraint means that `T` must implement the `Debug` trait,
/// allowing us to format it using the `{:?}` syntax.
fn print_debug<T: Debug>(t: T) {
    println!("{t:?}");
}

// This could also be written with a `where` clause:
fn print_debug2<T>(t: T)
where
    T: Debug,
{
    println!("{t:?}");
}

/// Struct with a type parameter.
///
/// `MyStruct<T>` is a struct that contains a single field `data` of type `T`.
struct MyStruct<T> {
    data: T,
}

/// Type parameters can constrain generic types.
///
/// Here, `U`, the type parameter of the function, is passed to the
/// `MyStruct<T>` generic type to guarantee that `T` equals `U`.
fn func2<U>(value: MyStruct<U>) -> U {
    value.data
}

/// Type parameters can have default types.
///
/// Here, `K` is the type of keys, `V` is the type of values, and `S` is the
/// type of the hasher. `S` has a default type of `RandomState`, which is a
/// common hasher type in Rust. This means that if you don't specify a hasher
/// type when creating a `HashMap`, it will use `RandomState` by default.
#[allow(unused)]
struct MyHashMap<K, V, S = RandomState> {
    inner: Vec<(K, V)>,
    hasher: S,
}

/// Generic implementation of a generic struct.
///
/// The type parameters `K`, `V`, and `S` are declared after the `impl` keyword,
/// indicating that this implementation is generic over these types.
impl<K, V, S> MyHashMap<K, V, S> {
    /// Creates a new `MyHashMap` with the specified hasher.
    fn new(hasher: S) -> Self {
        MyHashMap {
            inner: Vec::new(),
            hasher,
        }
    }
}

/// Trait with a type parameter.
trait Processor<T> {
    /// Trait type parameters can be used in associated functions, methods, and
    /// other items defined in traits.
    fn process(&self, item: T) -> String;
}

struct S;

// Generic implementation of a generic trait for a struct.
//
// The type parameter `T` is declared after the `impl` keyword, indicating
// that this implementation is generic over `T`, and used to set the type
// parameter of the trait.
impl<T> Processor<T> for S {
    fn process(&self, _item: T) -> String {
        unimplemented!();
    }
}

/// Let's define a tuple struct with a type parameter.
///
/// This is a struct that contains two fields of type `T`.
struct TupleStruct<T>(T, T);

// Generic implementation of a generic trait for a generic struct.
impl<T> Processor<T> for TupleStruct<T> {
    fn process(&self, _item: T) -> String {
        unimplemented!();
    }
}

/// Type alias with a type parameter.
///
/// `StringMap<K>` is a type alias for `HashMap<K, String>`.
/// This allows you to create maps where the keys can be of any type, but the
/// values are always `String`.
type StringMap<K> = HashMap<K, String>;

fn main() {
    // Use generic functions:
    //
    // The type of the generic parameter `T` is usually inferred by the compiler
    // from the type of the argument passed to the function. Here, `T` is
    // inferred to be `&str`:
    print_type("hello");
    // `T` is `i32`.
    print_type(42);
    // You could specify the type explicitly using the `turbofish` notation
    // `::<...>`.
    print_type::<f64>(3.1);

    // Use generic types with trait bounds.
    // `T` is inferred to be `i32`, which implements the `Debug` trait:
    print_debug(42);
    // ERROR: print_debug(S); // `S` does not implement the `Debug` trait.

    // Use generic structs:
    // `T` is inferred to be `bool` from the value passed to the field.
    let s = MyStruct { data: false };
    print_type(s);

    // `T` is `f64`.
    let tuple = TupleStruct(10.0, 20.0);
    print_type(tuple);

    // Use a generic type alias:
    // The turbofish notation `::<String>` is used to specify the type parameter
    // explicitly, since it cannot be inferred from the context.
    let _map = StringMap::<String>::new();

    // You could also write:
    let _map2: StringMap<String> = StringMap::new();
}

See also the Structs, Enums, Functions, and Traits sections for more details on how to use type parameters in those contexts.

Use Lifetime Parameters

"Lifetime generic parameters" (often just called "lifetime parameters" or "lifetimes") are another type of generics, similar to how you use T for a generic type, but instead of representing a type, they represent the scope for which a reference is valid.

  • Lifetime parameters start with an apostrophe (') and are typically lowercase, like 'a, 'b, 'c, etc.
  • They are declared in angled brackets (< >) just like regular type parameters.
  • Lifetime parameters must be listed first within < >, if mixed with type parameters or const generics.
  • They are then used to annotate references.

Think of them as annotations that describe how the lifetime of one reference relates to another. They don't change how long a reference actually lives; they simply provide the compiler with the necessary information to check if your usage of references is safe.

When the Rust compiler can't definitively infer the relationships between the lifetimes of references, especially in functions or structs that hold references, you need to explicitly tell it using lifetime generic parameters.

Lifetime parameters are often used when a type (for example, a struct) contains references. They are used to establish relationships between the lifetimes of the references and the containing type.

See the Lifetimes chapter for more details.

The following provides examples of lifetime parameters:

/// Struct with a type parameter `T` and a lifetime parameter `'a`.
///
/// The `T: ?Sized` trait bound indicates that `T` can be dynamically sized,
/// which will be useful in the example below.
struct DataHolder<'a, T: ?Sized> {
    data: &'a T, /* The lifetime parameter is used to specify the lifetime
                  * of the reference. */
}

/// Method implementation for the struct.
/// Note that the lifetime `'a` (declared after `impl`) is used in the struct
/// and in the method signature to ensure that the returned reference has the
/// same lifetime as the struct.
impl<'a, T> DataHolder<'a, T> {
    fn get_data(&self) -> &'a T {
        self.data
    }
}

fn main() {
    // `T` is `u8`.
    let data = DataHolder { data: &10u8 };
    println!("Data: {:?}", data.get_data());

    // The lifetime parameter is most often elided, but can be specified:
    let literal = "This string literal is of type &'static str";
    let _data: DataHolder<'static, str> = DataHolder { data: literal };
}

Const Generics

"Const generics" allow you to define generic parameters that are constant values rather than types or lifetimes. This means you can parameterize types, traits, and functions with compile-time known values.

Const generic parameters are declared in the angle brackets alongside type and lifetime parameters:

  • They start with the const keyword.
  • They require a type annotation (e.g., u32). The allowed types are currently limited to integer types (u8 to u128, i8 to i128, usize, isize), bool, and char.
  • You use them like regular constants within the item's definition.

Const generics allow you to:

  • Create data structures that have their size or other properties determined at compile time by a constant,
  • Implement traits for types based on constant values, i.e. define a single trait implementation that applies to all arrays (or your custom const-generic types) of a certain kind.
  • Perform compile-time checks and optimizations. Const generics can shift certain checks from runtime to compile time. For example, you could define a function that only accepts a matrix if its dimensions meet certain criteria (e.g., for matrix multiplication), catching errors earlier in the development process.

The following example shows how to define a struct that holds an array of a specific length, which is determined at compile time by the constant generic parameter N. The struct can then be used with arrays of different lengths, as long as they match the specified constant:

/// Struct with a const parameter.
///
/// Note the `const` keyword, the parameter name (here `N`),
/// which must be followed by a type declaration.
struct InnerArray<T, const N: usize>([T; N]);

impl<T: std::marker::Copy, const N: usize> InnerArray<T, N> {
    fn new(val: T) -> Self {
        InnerArray([val; N])
    }

    fn len(&self) -> usize {
        N // The size N is known at compile time.
    }
}

fn main() {
    // Use a type with a const generic.
    let array: InnerArray<i32, 3> = InnerArray::new(42);
    let len = array.len();
    println!("Array length: {len}");
    assert_eq!(len, 3);
}

The following example demonstrates how to implement traits for types based on constant values:

// Trait for a fixed-size buffer.
trait FixedSizeBuffer {
    const CAPACITY: usize;
    fn get_buffer(&self) -> &[u8];
}

// With const generics, we can implement this for any array of bytes:
impl<const N: usize> FixedSizeBuffer for [u8; N] {
    // The capacity is the array size.
    const CAPACITY: usize = N;

    fn get_buffer(&self) -> &[u8] {
        self
    }
}

fn process_buffer<T: FixedSizeBuffer>(buffer_provider: T) {
    println!("Processing buffer with capacity: {}", T::CAPACITY);
    println!("Buffer content: {:?}", buffer_provider.get_buffer());
}

fn main() {
    let buf1 = [10, 20, 30];
    let buf2 = [1, 2, 3, 4, 5, 6, 7, 8];

    process_buffer(buf1);
    process_buffer(buf2);
}

Related Topics

  • Enums.
  • Functions.
  • Lifetimes.
  • Rust Patterns.
  • Structs.
  • Traits.