RustBrock/Generics.md
2025-01-31 19:28:27 -07:00

8.8 KiB

Generic Data Types

These are used to create definitions for items like function signatures or structures, where it is then used with many different concrete data types

In Function Definitions

When defining a function that uses generics, yo place the generics in the signature of the funct.

This is usually in the place where concrete data types are specified (the parameters and return type)

This makes code more flexible and provide more functionality whilst preventing code duplication

Continuing with the finding the largest num in a i32 vector slice and a char vector slice

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
}

Here is the code before the use of generics

Notice the repeated code for a both types of slices

To parameterize the types in a signle function, the first thing is to name the type parameter (just like with concrete types)

You can use any identifier as a type parameter name but it is common to use T

Type parameter names in Rust are short, often just one letter and Rust's type-maning convention is UpperCamelCase

The T is the default choice of most Rust programmers

To use a generic it needs to be declared before it can be used

To delcare is place it inside <> and between the name of the function and the parameter list

Example

fn larget<T>(list: &[T]) -> &T {}

This function is read as: the function largest is generic over some type T. This function has one parameter named list, which is a slice of values of type T. The largest function will return a reference to a value fo the same type T.

Here is the updated, but this code will not compile

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

Here is the compilation error you will get

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

This mentions std::cmp::PartialOrd, thisis a trait.

For now know that this error states that the body of largest won't work for all possible types that T could be, becasue you cant compair against all possible values that T could be

To enable comparisions, the std library has the std::cmp::PartialOrd tait that you can implement on types.

By following the help text's usggestion you can restrict the types vaild for T to only those that implement PartialOrd, then this example will compile, becuase of the restriction that the std library implements PartialOrd on both i32 and char

In Struct Definitions

This can also be in a structure the generic also has to be defined in <> with the T being inside

Here is an example

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

This is similar to function definitions, after the generic is defined it can be used in the body

Note the use of only one generic type, this forces the use of only one concrete type when used in the entirity of the body

Here is a disallowed initialization

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

This is not allowed bacuse you are trying to assign both an i32 and a folating point integer, the struct definition does not allow this basue to only accpets one type per instance

You could use two generics in the definition

Here is an example where the use above would be allowed

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

Notice that x and y can now both be different concrete types.

You can use as many generic type parameters in a definition as wanted but more than a few makes it hard to read the code.

If you need lots of generic tpyes in your code, it indicates that yor code need restructing into smaller pieces

In Enum Definitions

Just like structs, it can also be used in enums.

An example that was used in the past was Option<T>

Here is an example in the form of the ``Result` enum that was unsed before sa well

enum Result<T, E> {
    Ok(T),
    Err(E),
}

The Result enum has two generics which typically hold the sucess value and an error type

If you recognize situations in your code with multiple struct or enum definitions that differ only in the types of the values they hold, thne you can avoid duplication by using generic types

In Method Definitions

You can implement generics in the associate methods of structs as well

Here is an example that uses the example form the Structures Section

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Not hat you have to delcare T right after impl so that we can use T to specify that we are implementing meothds on tpye Point<T>

By declaring T as a generic type after impl, Rust can identify that the type in the angle brackets in Point is a generic type rather than a concerete type.

You can choose a differnet nmae fo the generic than the original definition in the struct, using the same name is the standard

Methods written in that impl that delcares the generic will also be definined in any method in that impl scope no matter what concrete type is used in the definition

We can also specifiy constraints on generic types when deining methods on the type

For example lets say you have methods that you only want on Point<f32> instances rather than on Point<T> instances with a generic type

Here is an example of this

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Ths code means the tpye Point<f32> will have a distance_from_origin method and other methods of Point<T> that is not a f32

This method measures how far way the point is away from the coordinates (0.0, 0.0) and uses operations that are only available for floating-point types

Generic type parameters in a stuct def arent always the same as those you use in you in the same struct's methods signatures

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

This is used for deep copies where you dont know if the generic is the same as the original struct

This is an ample to show a situation where is is possible and used

It also shows how in the case that you need it

Perfomance of Code Using Generics

There is no cost at runtime for uing generics

Rust accomplishes this by performing monomorphization of the code using generics at compile time

Monomorphization is the proccess of turing generic code into specific code by filling in the concrete types that are used when complied

The compiler does the opposite of the steps we used o create the generic function

Here is an illustration of what the complier would do, just with different names

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}