Study Rust Bible analysis - Rust learn-10 (generics, traits, life cycle)

generic

Generics are abstract replacements for concrete types or other properties. We can express properties of generics, such as how they behave or how they relate to other generics, without knowing what they actually represent here when writing and compiling code, until code compiles, automatically helping us identify generics The real type of the type, called generic erasure, is called monomorphization in Rust, to ensure efficiency, so you don't need to worry about efficiency

apply generic methods

The generic method specifies the use of <T>letters of course can be any uppercase (standard) in general:

  • T: type type
  • K: key
  • V: value
use rand;
use rand::Rng;

fn rand_back<T>(list: &[T]) -> &T {
    
    
    let rand_num = rand::thread_rng().gen_range(0..3);
    return &list[rand_num];
}

fn main() {
    
    
    let arr = [5, 4, 3];
    let res = rand_back(&arr);
    println!("{}", res);
}

generic structure

Apply the type to the structure and set it in the field.
When using it later, we can directly specify the generic type used by the variable on the variable.

struct User<T> {
    
    
    id: T,
    username: T,
}

fn main() {
    
    
    let user:User<i32> = User {
    
    
        id: 32,
        username: 454,
    };
}

enum generic

enum Res<S, F> {
    
    
    Success(S),
    Fail(F),
}

Generics in method definitions

It is also possible to set generics for the implementation method of the structure? No, it should be because the structure is a generic type, so when implementing the method for the structure, you need to set it on the impl<T>

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

trait

A trait defines functionality that a particular type possesses that may be shared with other types. Shared behavior can be defined in an abstract way through traits. You can use trait bounds to specify that a generic is any type that possesses a specific behavior.

So in fact, you can think of trait as a Java interface, which needs to be implemented

define a trait

Use the trait keyword to define a trait, in which we define the methods we need to implement by the subclass without specifying the specific behavior of the method

trait Service{
    
    
    fn init();
    fn do_job();
    fn finish();
}

Default trait method implementation

I don’t know if you remember that there is a default keyword in the Java interface, which is used to implement the methods in the interface by default. Subsequent subclasses can choose to override, or use the default method of the interface. In fact, Rust also has this feature.

But you don’t need any keywords to declare, you can just write it directly:

trait Service {
    
    
    fn init();
    fn do_job();
    fn finish() {
    
    
        println!("all services are closed");
    }
}

Implement traits for structs

impl trait for structImplement traits as structs by using

impl Service for User<i32> {
    
    
    fn init() {
    
    
        todo!()
    }

    fn do_job() {
    
    
        todo!()
    }
}

Call the method implemented in the trait

The calling method mainly depends on whether the trait uses self as an input parameter. Only variables that can be assigned with self as an input parameter are called. .Let
’s look at an example:

struct User<T> {
    
    
    id: T,
    username: T,
}

trait Service {
    
    
    fn init(&self);
    fn do_job();
    fn finish() {
    
    
        println!("all services are closed");
    }
}

impl Service for User<i32> {
    
    
    fn init(&self) {
    
    
       println!("do init")
    }

    fn do_job() {
    
    
        println!("do job")
    }
}

fn main() {
    
    
    let user: User<i32> = User {
    
    
        id: 32,
        username: 454,
    };
    user.init();
    User::finish()
}

It can be clearly found from the example that the input parameter can only be used without self::

takes the trait as a parameter

This is also a very common way of writing. We require that only entities that implement a certain trait can be used as input parameters. Applying the public methods in it, we can see that both
User and User2 have implemented Service. When we pass in parameters later, we can pass User You can also pass User2

struct User<T> {
    
    
    id: T,
    username: T,
}

struct User2{
    
    
    id:i32
}

trait Service {
    
    
    fn init(&self);
    fn do_job();
    fn finish() {
    
    
        println!("all services are closed");
    }
}

impl Service for User<i32> {
    
    
    fn init(&self) {
    
    
       println!("do init")
    }

    fn do_job() {
    
    
        println!("do job")
    }
}

impl Service for User2{
    
    
    fn init(&self) {
    
    
        todo!()
    }

    fn do_job() {
    
    
        todo!()
    }
}

fn use_Service(impl_instance:&impl Service){
    
    
    impl_instance.init();
}

trait bound

We think this is just syntactic sugar for traits, and its application scenarios lie in scenarios where a large number of trait parameters need to be passed in

fn test<T: Service>(item1: &T, item2: &T) {
    
    

equivalent to

fn test(item1: &impl Service, item2: &impl Service) {
    
    

Multiple implementations of input parameter binding

In our real program, it is impossible for a struct to implement only one trait. This problem is known to everyone who has studied design patterns. We often abstract the implementation required by a module, and then let a module choose a certain one it needs. or some implementation

We can use +operators to limit binding parameters

fn test(param:&(impl Service1 +  Service2))

This shows that the input parameter of param needs to implement both Service1 and Service2.
We can also abbreviate through bound

fn test<T:Service1+Service2>(param:&T)

where abbreviation

However, there are downsides to using too many trait bounds. Each generic type has its own trait bound, so functions with multiple generic parameters will have long trait bound information between the name and the parameter list, which makes the function signature difficult to read.

as follows:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
    
    

We can use the where shorthand (my favorite)

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    
    

Multiple bound binding implementation

Let's think about it: If we don't have a type like HashMap, and our requirement is to construct it through a structure and output the following results:

User {
    
    
    id: KV {
    
    
        key: 1,
        value: "1001",
    },
    username: KV {
    
    
        key: 2,
        value: "zhangsan",
    },
}

Then we need to build a struct called KV, and set the field names of key and value, as follows:

#[derive(Debug)]
struct User {
    
    
    id: KV,
    username: KV,
}
#[derive(Debug)]
struct KV {
    
    
    key: i32,
    value: String,
}

fn main() {
    
    
    let user = User {
    
    
        id: KV {
    
    
            key: 1,
            value: "1001".to_string(),
        },
        username: KV {
    
    
            key: 2,
            value: "zhangsan".to_string(),
        },
    };

    println!("{:#?}", user);
}

This can be achieved directly. Now there is a requirement. We hope that the value type of KV can not only store String, but also store other types. We can think of:

#[derive(Debug)]
struct User<K,V> {
    
    
    id: KV<K>,
    username: KV<V>,
}
#[derive(Debug)]
struct KV<T> {
    
    
    key: i32,
    value: T,
}

fn main() {
    
    
    let user = User {
    
    
        id: KV {
    
    
            key: 1,
            value: 1001,
        },
        username: KV {
    
    
            key: 2,
            value: "zhangsan".to_string(),
        },
    };

    println!("{:#?}", user);
}

This step is also very simple, but we will start to find some problems. If we need to constrain some generic types, it will become very complicated and difficult to read.
We need to be simpler, more general, and more binding How to write:

impl<T:Service1+Service3> KV<T>{
    
    
	//....
}

In this way, the incoming T of the KV structure needs to implement Service1+Service3.
The following is a specific example:
the a variable cannot use the show method

impl<T> Service1 for KV<T> {
    
    }

impl<T> Service3 for KV<T> {
    
    
    fn service3(&self) {
    
    
        println!("service3")
    }
}

impl<T: Service1 + Service3> KV<T> {
    
    
    fn show(&self) {
    
    
        println!("{}", self.key);
    }
}



fn main() {
    
    
    let a = KV {
    
    
        key: 1,
        value: "shd".to_string(),
    };
    a.service1();
    a.service3();

    let b = KV {
    
    
        key: 1,
        value: KV{
    
    
            key: 2,
            value: "shd".to_string(),
        }
    };

    b.show();

}

life cycle

Lifecycles are another class of generics that we've already used. Instead of ensuring that a type behaves as expected, a lifetime ensures that a reference remains valid as expected.

Lifecycle avoids dangling references

The main goal of the life cycle is to avoid dangling references (dangling references), which can cause the program to refer to data that is not intended to be referenced.

fn main() {
    
    
    let r;

    {
    
    
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

r = &x;We see that it is included in a new scope in this program , which causes r to still have no value when it is printed at the end. The statement of conversion into life cycle is:

  1. let r: created the r variable
  2. {: enter a new scope
    1. let x = 5 : Create a variable and assign a value of 5
    2. r: reference to x
  3. }: exit scope, x life cycle ends
  4. println!("r: {}", r): print

We can see that the corresponding x out of scope is destroyed and the life cycle ends, but r does not end, and still exists when printing, but the reference is destroyed along with the value, resulting in a dangling

Therefore, in order not to overhang, the life cycle of x must be extended

Define life cycle

We only need to define the life cycle for a variable, and 'ayou can actually write any letters or words (except static, etc. keywords, because it is a static life cycle, which will be described later). The life cycle is generally set on the method, in order to specify Collection after scope ends

Let's look at a program:

fn main() {
    
    
    let a = "sjd";
    let b = "absc";
    let res = show(a,b);
    println!("{}", res)
}

fn show(s: &str,b:&str) -> &str {
    
    
    return if s.len() > b.len() {
    
    
        b
    } else {
    
    
        a
    }
}

It's very simple, pass in a string slice, judge the length and return, it's simple, but an error is reported when compiling!
insert image description here
You will think, I usually pass a string slice in and return it out without any problem. For example:

fn main() {
    
    
    let a = "sjd";
    let b = "absc";
    let res2 = show2(a);
    println!("{}", res2);
}

fn show2(s: &str) ->&str{
    
    
    s
}

That's right, this function is of course no problem, because the compiler knows that it must return s in the end, but in the above program, the compiler does not know which one is returned in the end. When we define this function, we don't know the specific value passed to the function. , so I don't know if or else will be executed. We also don't know the specific lifetime of the references passed in, so problems naturally arise.
So we need to display the annotation life cycle to help the compiler understand

Annotation life cycle

As follows, we mark the life cycle of the parameters used in the function, and mark the return value, which is equivalent to telling the compiler that the two parameters have the same long life cycle. The life cycle of the reference returned by the function is the same as that referenced by the function parameter. The smaller of the lifetimes of the values ​​agrees

fn main() {
    
    
    let a = "sjd";
    let b = "absc";
    let res = show(a, b);
    println!("{}", res);
}

fn show<'a>(s: &'a str, b: &'a str) -> &'a str {
    
    
    return if s.len() > b.len() {
    
    
        b
    } else {
    
    
        s
    };
}

That is to say, the life cycle of the return value of this function is consistent with the life cycle of the smaller one of the input parameters, so that until the life cycle of the smaller one ends, the life cycle of the return value will also end

Structure Lifecycle

In addition to marking the life cycle on the method, the life cycle can also be marked on the structure

struct ImportantExcerpt<'a> {
    
    
    part: &'a str,
}

This structure has only one field part, which stores a string slice, which is a reference. Similar to generic parameter types, generic lifetime parameters must be declared in angle brackets after the struct name in order to use lifetime parameters in the struct definition. This annotation means that an instance of ImportantExcerpt cannot outlive the reference in its part field.

output/input life cycle

The lifetime of the parameters of a function or method is called the input lifetime, and the lifetime of the return value is called the output lifetime.

life cycle rules

https://kaisery.github.io/trpl-zh-cn/ch10-03-lifetime-syntax.html

The compiler uses three rules to determine when a reference does not require an explicit annotation. The first rule applies to input lifecycles, and the last two rules apply to output lifecycles. If the compiler checks these three rules and there are still references that have not calculated the lifetime, the compiler will stop and generate an error

  1. The compiler assigns a lifetime parameter to each reference parameter. In other words, a function with one reference parameter has one lifetime parameter: fn foo<'a>(x: &'a i32), and a function with two reference parameters has two different lifetime parameters, fn foo<'a, 'b>(x: &'a i32, y: &'b i32), and so on
  2. If there is only one input lifetime parameter, then it is given all output lifetime parameters: fn foo<'a>(x: &'a i32) -> &'a i32. (This is why we don't need to display the declaration lifecycle for single-argument functions)
  3. If the method has multiple input life cycle parameters and one of the parameters is &self or &mut self, indicating that it is an object method, then all output life cycle parameters are given the life cycle of self

static life cycle

'static, whose lifetime survives the entire program. All string literals have a 'static lifetime

Before assigning a reference to 'statica , think about whether the reference is really valid for the lifetime of the program, and whether you want it to last that long. In most cases, error messages recommending 'static lifetimes are the result of trying to create a dangling reference or mismatching available lifetimes. The solution in this case is to fix these problems instead of specifying a lifetime 'staticof

Guess you like

Origin blog.csdn.net/qq_51553982/article/details/130209972