Rust Generics

Rust generic struct

In Rust, generics are a powerful language feature that allows us to write functions, data types, and structs that can work with any type, instead of being limited to a specific type.

Generics are implemented using type parameters, <T>.

Type parameters are placeholders for the actual data types that are provided when the code is used.

For example, the following function can accept any data type as an argument:

fn accepts_all_types<T>(x: T) {}

When we call this function later, we can pass in a variety of types:

accepts_all_types(42);     // Passing in an int  
accepts_all_types(a);      // Passing in a char
accepts_all_types("Hi!");  // Passing in a string

Rust allows us to write code that is both flexible and reusable using generics. This is because we can write generic functions and data structures that can work with any type. Generics help make Rust a highly expressive language, and is one of the reasons that Rust is a popular choice for systems programming. In all applications, generic code can help improve performance and reduce code duplication.

In this tutorial, we cover what generics are, how they work, and how to use them in any Rust project.

What Are Rust Generics?

Generics are one of the primary methods by which Rust achieves parametric polymorphism. This means that a single piece of code can be written using type parameters instead of specific types.

When that code later gets called, the compiler is able to work with any type in order to execute the code on it.

In Rust, generics have wide applications and can be used with functions, structs, traits, enums, and collections. We will cover several use cases below.

Generic Functions in Rust

Functions are a common application for generics because there may be different situations in which we want to perform the same operation on different types.

One common example is using functions to perform mathematical operations. Rust won’t natively allow us to specify a ‘number’ type, and it won’t allow us to use a function to perform the same operation on different numeric types. This can be addressed using generics.

Functions With Generics For Numeric Types

In the following example, we create a function called larger_number() that compares two numbers of any numeric type. We then pass this function integers and floats to demonstrate the concept:

fn larger_number<T: PartialOrd>(x: T, y: T) -> T {
    if x > y {
        x
    } else {
        y
    }
}

fn main() {
    let a = 21;
    let b = 42;
    let larger_int = larger_number(a, b);
    println!("The larger integer is: {}", larger_int);

    let c = 6.1;
    let d = 4.2;
    let larger_float = larger_number(c, d);
    println!("The larger float is: {}", larger_float);
}

Standard Output:

The larger integer is: 42
The larger float is: 6.1

We can see that the larger_number() function is able to take both integers or floats using the type parameter.

Note: In order for this code to run, we needed to restrict the type parameter using the PartialOrd trait. This specifies that the type must be one that is able undergo binary operations, enabling us to use the greater than ‘>’ comparison operator.

Generic Functions For Any Type

Functions aren’t limited to numeric types; depending on the operation they perform, they can essentially accept any type.

The following function concatenates any two arguments regardless of the type:

use std::fmt::Display;
fn concatenate_anything<T:Display>(x:T, y:T){
    let result = format!("{}{}", x , y);
    println!("{}", result);
}

fn main(){
    println!("Passing two strings:"); 
    concatenate_anything("Hello, ", "World!"); 
    println!("Passing two integers:"); 
    concatenate_anything(42, 4);
}

Standard Output:

Passing two strings:
Hello, World!
Passing two integers:
424

Using Generics With Vectors

Generics can be used with vectors, allowing us to operate on collections of data regardless of the type of that data.

In the following example, we are using the print_any_vec() function with generic types to iterate over two vectors of different types with the .iter() method:

use std::fmt::Display;
fn print_any_vec<T:Display>(v: &[T]) {
  for i in v.iter() {
      print!("{}", i)
  }
  println!("");
}

fn main() {
    let int_vec = [1, 2, 3, 4];
    println!("Call function with vector of integers");
    print_any_vec(& int_vec); 

    let str_vec = ["Hello, ", "World!"];
    println!("Call function with vector of strings");
    print_any_vec(&str_vec);
}

Standard Output:

Call function with vector of integers
1234
Call function with vector of strings
Hello, World!

The print_any_vec() function takes in one vector comprised of integers and another comprised of strings, and is able to print both.

Using Generics With Structs

Generics can be used with structs to significantly expand their capabilities.

When using a struct with type parameters, we use the following syntax:

struct StructName<T> {
    field1: T,
    field2: T,
}

When we instantiate the struct, we need to specify the type. The values for each field in the instance need to match the type specified.

let struct1:type = StructName{field1:value1, field2:value2}

Let’s see how this looks in a working example. In the following code, we first declare a struct named Rectangle. Rectangle has two fields: length and width.

struct Rectangle<T> {
    length: T,
    width: T,
}

fn main() {
    let rec1:Rectangle<i32> = Rectangle{length:5, width:10};
    println!("Rectangle struct with type integer i32:");
    println!("Length:{}, Width:{}", rec1.length, rec1.width);

    let rec2:Rectangle<f32> = Rectangle{length:12.1, width:3.3};
    println!("Rectangle struct with type float f32:");
    println!("Length:{}, Width:{}", rec2.length, rec2.width);
   
}

Standard Output:

Rectangle struct with type integer i32:
Length:5, Width:10
Rectangle struct with type float f32:
Length:12.1, Width:3.3

The Option Enum

Generics can also be used with enums, and the Option enum is an excellent example of this.

The Option enum has two variants: Some(T) and None, where T represents a generic type. We can see this more clearly in the code for the Option enum:

enum Option<T> {
    Some(T),
    None,
}

Note that the Option enum uses the type parameter <T> to denote a generic type. This allows it to work with any data type, making it a robust way of working with a variety of situations when there may not be a return value.

In this example, the use of a generic type allows the Option enum to be used regardless of the type of the value.

When we want to use the Option enum, we do so using a similar syntax to the previous examples:

let x: Option<i32> = Some(42);