Trust learning: generics data types

Time:2021-2-25

We can use generics to create definitions for items such as function signatures or structures, which can then be used for many different concrete data types. First, let’s look at how to use generics to define functions, structures, enumerations, and methods. Then we’ll discuss how generics affect code performance.

In function definition
For example, if we define two functions, i.e. the maximum value and the maximum string, and require that all the parameters passed in are an array, we may implement them as follows:

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 maximum number is: {}", result); // 100

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

    let result = largest_char(&char_list);
    Println! "The maximum character is: {}", result); // Y
}

From the above example, we can see that although the parameter types of the two function methods are inconsistent, their behaviors are the same or similar. So do we extract a common function class? If we can, this method can be called generic
fn largest<T>(list: &[T]) -> T {

We read this definition as: the largest function is generic on some type T. This function has a parameter called list, which is part of the value of type T. The largest function will return a value of the same type T.

The following example shows the definition of the maximum combination function using a common data type in a signature. The listing also shows how to call a function using an I32 value slice or char value. Note that the code has not been compiled.

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

If we compile the code now:

D:\learn\cargo_learn>cargo run
   Compiling cargo_learn v0.1.0 (D:\learn\cargo_learn)
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 {
  |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `cargo_learn`.

To learn more, run the command again with --verbose.

It is mentioned in the notesstd::cmp::PartialOrdThis is a feature. Currently, this error indicates that the largest body does not apply to all types that t might apply to. Because we want to compare the values of type T in the principal, we can only use types whose values can be sorted. For comparison, the standard library hasstd::cmp::PartialOrdFeatures that we can implement on types.

In structure definition
We can also use the < > syntax to define a structure to use generic type parameters in one or more fields. The following example shows how to define a point < T > structure to hold any type of X and Y coordinate values:

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 syntax for using generics in structure definitions is similar to that used in function definitions. First, we declare the name of the type parameter in angle brackets after the structure name. We can then use generic types in the structure definition, otherwise we will specify specific data types.

Note that since we use only one generic type to define point < T >, this definition means that the point < T > structure is generic on some types of T, and the fields X and y are of the same type, regardless of the type. If we create an instance of point < T >, which has different types of values, our code will not compile as shown in the following example:

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

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

Since X and y are not of the same type, when compiling:

$ cargo run
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

error: aborting due to previous error

To define a point structure, where x and y are generic but can have different types, we can use multiple generic type parameters. For example, in the following example, we can change the definition of point to be common on types T and u, where x is type T and Y is type U:

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

Now all point instances can be displayed! You can use as many generic type parameters as you want in the definition, but using multiple generic type parameters makes the code hard to read. When a large number of generic types are required in the code, this may indicate that the code needs to be reorganized into smaller parts.

In the enumeration definition
Let’s take a look at an instance of an enumeration type


#![allow(unused_variables)]
fn main() {
    enum Option<T> {
        Some(T),
        None,
    }
}

This enumeration can be used to define any type of some, such as:let y: Option<i8> = Some(5);Similar to struc, we can define the generics of enumeration types


#![allow(unused_variables)]
fn main() {
    enum Result<T, E> {
        Ok(T),
        Err(E),
    }
}

The result enumeration is universal to T and E, and has two variants: OK (with a value of type T) and err (with a value of type E). This definition makes it easy to use the result enumeration anywhere. The operation we perform may succeed (return a value of type T) or fail (return an error of type E). In fact, this is the file used to open the file in the previous error handling, where when the file is successfully opened, t is filled withstd::fs::FileType, and E is filled withstd::io::ErrorThere was a problem opening the file.

When multiple structure or enumeration definitions are identified in code, they only differ in the types of values they hold, so you can avoid repetition by using generic types.

In method definition
We can implement methods on structure and enumeration (as we did in Chapter 5), and we can also use generic types in their definitions. The example is as follows:

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

D:\learn\rust_test>cargo run
   Compiling rust_test v0.1.0 (D:\learn\rust_test)
warning: field is never read: `y`
 --> src\main.rs:3:5
  |
3 |     y: T,
  |     ^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: 1 warning emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target\debug\rust_test.exe`
p.x = 5

Note that we have to declare t after impl so that we can use it to specify the method we want to implement on the point < T > type. By declaring t as a generic type after impl, rust recognizes that the type in angle brackets in point is generic, not concrete.

For example, we can implement methods only on point < F32 > instances, not on point < T > instances with any generic types. In the following example, we use the concrete type F32, which means that we do not declare any types after impl.

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

This code means that the point < F32 > type will have a value called distance_ from_ This method is not defined in other instances of point < T >, where t is not of type F32. This method measures the distance between our point and the point at coordinates (0.0, 0.0), and uses mathematical operations only for floating-point types.

The generic type parameters in the structure definition are not always the same as those we use in the method signature of the structure. For example, the point < T, u > structure in the following example defines method mixing. The method takes another point as a parameter, which may be different from the type of self point that we call the hybrid. This method creates a new point instance using the x value of the self point (type T) and the y value of the incoming point (type W)

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

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        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);
}
D:\learn\rust_test>cargo run
   Compiling rust_test v0.1.0 (D:\learn\rust_test)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target\debug\rust_test.exe`
p3.x = 5, p3.y = c

The purpose of this example is to demonstrate a situation where some generic parameters are declared with impl and others are declared with method definitions. Here, the common parameters T and u are declared after impl because they are used with the struct definition. The generic parameters V and W are declared after FN is mixed, because they are only method related.

Code performance using generics

We may want to know if there are run-time costs when using generic type parameters. The good news is that rust implements generics in such a way that code that uses generic types does not run slower than code that uses specific types.

Rust achieves this by monomorphizing code that uses generics at compile time. Singleton is the process of transforming general code into specific code by filling in specific types used at compile time.

In this process, the compiler does the reverse of the steps used to create generic functions in the following example: the compiler looks at all places where generic code is called and generates code for the specific type of generic code that is called.

Let’s see how an example of using the option < T > enumeration of the standard library works:

#![allow(unused_variables)]
fn main() {
    let integer = Some(5);
    let float = Some(5.0);
}

When rust compiles this code, it performs a singleton. In this process, the compiler reads the values used in the option < T > instance and identifies two options < T >: I32 and f64. In this way, it extends the general definition of option < T > to option_ I32 and option_ In order to replace the general definition with a specific one.

The singleton version of the code is shown below. General option < T > is replaced with a specific definition created by the compiler:

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

Because rust compiles generic code into code of the type specified in each instance, we don’t have to pay any runtime cost for using generic code. When the code runs, its performance is the same as when we manually copy each definition. The simplification process makes rust’s generics very efficient at run time.

Recommended Today

Redis design and implementation 4: Dictionary Dict

In redis, the dictionary is the infrastructure. Redis database data, expiration time and hash type all take the dictionary as the underlying structure. Structure of dictionary Hashtable The implementation code of hash table is as follows:dict.h/dictht The dictionary of redis is implemented in the form of hash table. typedef struct dictht { //Hash table array, […]