The Rust Programming Language - Chapter 10 generics, trait, and lifecycle - 10.1 generic data types

Keywords: Rust

10 generics, trait, and lifecycle

Every programming language has tools to deal with repetitive concepts efficiently, and Rust uses generics. Generics are alternatives to concrete types or other abstract types. We can express the properties of generics, such as how their behavior is associated with other types of generics, without knowing what they actually represent here when writing or compiling code

We write a function to handle multiple types of parameters, not just to define a function for a specific type of parameters. At this time, we can specify that the parameter type of the function is generic rather than a specific type. We used Option and Vec before! And HashMap < K, V >

Extract functions to reduce duplication

Before that, let's review a technique to solve code duplication without using generics, extracting functions

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

    for number in number_list {
        if number > largest  {
            largest = number
        };
    };
    println!("the largest number is {}",largest)
}

We first traverse a vector to find the maximum value of its element. If we want to traverse another element, we need to repeat these codes. Now we use the function to extract

fn largest(list:&[i32])->i32{
    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!("{}",result)
}

Through function abstraction, we don't need to reuse the code, just call the comparison logic (our newly defined function)

But! What we are looking for here is the maximum value in vec. What if we are looking for the maximum value in char and slice? Let's start to solve this problem

10.1 generic data types

When we use generic parameters to define function signatures, we can use generic data types in both function parameters and return values, which will make the code more adaptable

In order to find the largest i32 and char in slice, we define two functions, as follows

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

    for &item in list.iter(){
        if item > largest{
            largest = item;
        }
    }
    largest
}

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

    for &item in list.iter(){
        if item > largest{
            largest = item;
        }
    }
    largest
}
fn main(){
    let number_list = vec![34,50,25,100,65];
    let result = largest_i32(&number_list);
    println!("{}",result);// 100

    let char_list = vec!['y','m','a','q'];
    let result = largest_char(&char_list);
    println!("{}",result)// y
}

But in fact, we can see that the function bodies of the two functions are the same. Now let's use generic simplification (note the parameters and return values)

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

    for &item in list.iter(){
        if item > largest{
            largest = item;
        }
    }
    largest
}

And we implement it in the main function (but it can't be compiled at this time)

fn main(){
    let number_list = vec![34,50,25,100,65];
    let result = largest(&number_list);
    println!("{}",result);// 100

    let char_list = vec!['y','m','a','q'];
    let result = largest(&char_list);
    println!("{}",result)// y
}

The operation output is as follows:

error[E0369]: binary operation `>` cannot be applied to type `T`
  --> src\main.rs:36:17
   |
36 |         if item > largest{
   |            ---- ^ ------- T
   |            |
   |            T
   |
help: consider restricting type parameter `T`
   |
32 | fn largest<T: std::cmp::PartialOrd>(list:&[T]) -> T{

std::cmp::PartialOrd is a trait. We will talk about it in the next section. Now we only need to understand it. It means that the function body of large cannot be applied to all possible types of T, because the value of T type needs to be compared in the function body, but it can only be used for types we know how to sort. To enable the comparison function, std::cmp::PartialOrd defined in the standard library

trait can implement the function of type comparison

Generics in struct definitions

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};
}

The x and y types here must be the same, but if you want to define multiple generic parameters, you can refer to the following code:

struct Point<T,U> {
    x:T,
    y:U,
}
fn main(){
    let integer_and_float = Point{x:5,y:4.0};
}

Generic types in enumeration definitions

Like structs, enumerations can also store generic data types in members. Let's review Option, which stores a value of type T

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

Enumerations can also have multiple generic types, T and E

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

In fact, T corresponds to std::fs::File type. When there is a problem opening the file, the E value is std::io::Error type

When you realize that multiple enumerations or structures are defined in the code, you can try using generics to avoid repetition

Generics in method definitions

struct Point<T,U> {
    x:T,
    y:U,
}
impl<T,U>Point<T,U> {
    fn x(&self)->&T {
        &self.x
    }
}
fn main(){
    let p = Point{x:5,y:10};
    println!("p.x = {}",p.x());
}// p.x = 5

The generic logic in the method definition is roughly the same as that in other definitions

Performance of generic code

The good news is that there is no performance penalty for programs that use generic types as arguments and concrete types as arguments. Why? This is because Rust ensures efficiency by monomatizing generic code at compile time. Monomatization is a process of converting general code into specific code by filling in specific types used at compile time

That is, we create a generic type, and the compiler converts the generic type to a specific type. Let's take an example in the Option enumeration:

let integer =  Some(5);
let float = Some(5.0);

When Rust compiles these codes, he will singlet them, and the compiler will read the value passed to Option. It finds that there are two types, one i32 type and the other f64 type, so it will be expanded into Option_i32 and Option_ Then replace the generic definition with these two specific definitions

The code generated by the compiler looks like the following:

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);
}

We use generics to write non repetitive code, and Rust will compile the code of its top type for each instance. This means that there is no running overhead when using generics, and the code running efficiency is just like our handwritten repeated code. This singleton process is the reason why Rust is efficient at run time

Posted by vh3r on Tue, 16 Nov 2021 17:02:13 -0800