Type system and traits related

The type system is actually a system that defines, checks and processes (converts) types (int, float, vec, enum, etc.).
The role of the type system is to reduce the mental burden of programming and allow developers to think at a higher level, such as class abstraction, troubleshooting errors, and ensuring memory safety.
Some simple classifications
: strong type/weak type.
According to whether implicit conversion is possible , it can be divided into strong type and weak type. Rust cannot automatically convert between different types, so it is a strongly typed language, while C / C++ / JavaScript can automatically convert, so it is a weakly typed language.

Static type system/dynamic type system
According to the timing of type checking, it can be divided into static type system (compile-time checking) and dynamic type system (run-time checking, such as python, js).
Why is rust memory safe?
The ownership system is one point, the type system is another important point.
And rust is statically strongly typed . Errors can be found during compilation, and they cannot be implicitly converted at will, avoiding many unexpected errors.
Moreover, in order to achieve strict type safety, Rust, except for let/fn/static/const qualitative statements, are all expressions. Expressions always have values, and statements do not necessarily have values. If there is a value, there is a type. So it can be said that everything in Rust is a type.
In addition to some basic primitive types and composite types, Rust also incorporates scope into the type system. This is the life cycle mark that you will learn in Chapter 4. There are also some expressions that sometimes return a value and sometimes do not (that is, only return the unit value), or sometimes return the correct value and sometimes return the wrong value. Rust also incorporates this type of situation into the type system, that is Optional types such as Option<T> and Result<T, E> force developers to handle these two situations separately . Some situations that cannot return a value at all, such as thread crash, break or continue, have also been included in the type system. This type is called the never type . It can be said that Rust's type system basically covers various situations encountered in programming . Under normal circumstances, there will be no undefined behavior . Therefore, Rust is a type-safe language. As long as the types are defined, these types can be processed without undefined behavior.

Polymorphism
is a very important idea in the type system. It means that when using the same interface, different types of objects will adopt different implementations.
For static type systems (C++, JAVA, RUST, etc.), polymorphism can be achieved through parametric polymorphism (parametric polymorphism) and ad hoc polymorphism (adhoc polymorphism) traits. In C++, it generally refers to function overloading. , and subtype polymorphism (subtype polymorphism) implementation (that is, the subclass parent class virtual function implementation in C++, rust uses trait object to support).

Type inference
Within a scope, Rust can deduce the type of variables based on context. This makes rust as convenient as python.
But it won’t work if it’s not in the same scope, you have to write it yourself.

Rust's generics (parametric polymorphism)
include generic data structures and generic functions.
enum Option { Some(T), None, } This means that T is of any type. When Option has a value, it is Some(T), and when it has no value, it is None. The process of defining this generic structure is a bit like defining a function: a function extracts parameters from repeated code to make it more versatile. When calling a function, we get different results depending on the parameters; while generics, It extracts the parameters from the repeated data structure. When using generic types, we will get different specific types according to different parameters.






Generic functions
Now that we know how to define and use generic data structures, let's look at generic functions. Their ideas are similar. When declaring a function, we can also not specify specific parameters or return value types, but use generic parameters instead.
fn id(x: T) -> T { return x; } fn main() { let int = id(10); let string = id("Tyr"); println!("{}, {}", int , string); } Rust will monomorphize generic functions . The so-called monomorphic processing is to expand the generic parameters of the generic function into a series of functions during compilation. The advantages and disadvantages of monomorphism are obvious: Advantages: The calling of generic functions is statically distributed, and one-to-one correspondence is achieved at compile time. It has the flexibility of polymorphism without any loss of execution efficiency. ** is a zero-cost abstraction. Disadvantages: Compilation speed is very slow . For a generic function, the compiler needs to find all the different types used and compile them one by one. That’s why people always complain about the speed at which Rust compiles code.











Traits and ad hoc polymorphism.
Ad hoc polymorphism: including operator overloading , refers to many different implementations of the same behavior ;
if we want to define a file system, it is important to decouple the system from the underlying storage. File operations mainly include four: open, write, read, and close. These operations can occur on the hard disk, in memory, or in network IO. In short, if you have to implement a separate set of code for each situation, the implementation will be too complicated and unnecessary.

If different types have the same behavior, then we can define a trait and then implement the trait for these types

#![allow(unused)]
fn main() {
    
    
pub trait Summary {
    
    
    fn summarize(&self) -> String;
}
pub struct Post {
    
    
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
}
impl Summary for Post {
    
    
    fn summarize(&self) -> String {
    
    
        format!("文章{}, 作者是{}", self.title, self.author)
    }
}
pub struct Weibo {
    
    
    pub username: String,
    pub content: String
}
impl Summary for Weibo {
    
    
    fn summarize(&self) -> String {
    
    
        format!("{}发表了微博{}", self.username, self.content)
    }
}
}

Orphan Rule
If you want to implement trait T for type A, then at least one of A or T is defined in the current scope! It ensures that code written by others will not break your code, and it also ensures that you will not break unrelated code inexplicably.

Default implementation
You can define a method in a trait with a default implementation so that other types do not need to implement the method, or you can choose to override the method:

( First of all, traits implement ad hoc polymorphism, and the advantages are similar to function overloading in C++. That is, if different types have the same characteristics, the common characteristics are extracted into traits. Each type has a different implementation. At this time, static distribution and compilation It is definitely a zero-cost abstraction, and there is no need to judge during runtime.
The following is the more powerful aspect of traits, that is, traits can be used as parameters and constraints, but C++ function overloading cannot . This can make our code more scalable, such as C++ wants To add a new overload, you need to add a new definition to the interface, and the trait does not need to be modified)

Using traits as function parameters
To be honest, if the characteristics are just that, you may feel that the bells and whistles are useless. Next, let you see the true power of traits. If features are only used to implement methods, they are really overkill.

pub fn notify(item: &impl Summary) {
    
    
    println!("Breaking news! {}", item.summarize());
}

impl Summary, I can only say that the person who came up with this type is really a genius at naming it. It is so appropriate. As the name suggests, it means the item parameter that implements the Summary feature.
You can use any type that implements the Summary trait as a parameter of this function, and within the function body, you can also call methods of this trait, such as the summarize method. Specifically, you can pass an instance of Post or Weibo as a parameter, but other types such as String or i32 cannot be used as parameters of this function because they do not implement the Summary feature.

Trait bound
Although the impl Trait syntax is very easy to understand, it is actually just syntactic sugar:

pub fn notify<T: Summary>(item: &T) {
    
    
    println!("Breaking news! {}", item.summarize());
}

The real complete written form is as mentioned above, in the form of T: Summary, which is called a feature constraint.
In simple scenarios, the syntax sugar of impl Trait is enough, but for complex scenarios**, feature constraints can give us greater flexibility and syntax expression capabilities. For example, a function accepts two impl Summary parameters: **
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
If the two parameters of the function are of different types, then the above method is good, as long as both types implement the Summary feature. But what if we want to force the two parameters of the function to be of the same type? **The above syntax cannot achieve this restriction. At this time, we can only use feature constraints to achieve it:
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
Generic type T describes item1 It must have the same type as item2, and T: Summary indicates that T must implement the Summary feature.

Multiple constraints
In addition to a single constraint, we can also specify multiple constraints. For example, in addition to letting the parameter implement the Summary feature, we can also let the parameter implement the Display feature to control its formatted output: pub fn notify(
item: &(impl Summary + Display)) {}
In addition to the above syntactic sugar form, you can also use the feature constraint form:
pub fn notify<T: Summary + Display>(item: &T) {}
With these two features, you can use item.summarize method, and format the output item through println!(“{}”, item).
Where constraints
When the feature constraints become many, the signature of the function will become very complicated:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
strictly That said, the above example is still not complex enough, but we can still make some formal improvements to it, through where:

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

Characteristics and characteristic constraints are very important concepts, and they implement ad-hoc polymorphism, which is function overloading. The characteristic is to define a common set of behaviors and make interface classes. Different types can implement these behaviors specifically. This achieves ad-hoc. The same function signature can be used with different types of parameters, but the specific implementation is different.
Characteristic constraints allow traits to be used as parameters of functions, making this polymorphism more flexible. Only types that implement traits can be used as parameters of functions.

Trait Objects and Subtype Polymorphism
If we want to implement a UI component with different elements (buttons, text boxes, etc.). They all exist in a table and need to be rendered on the screen one by one using the same method!
In languages ​​with inheritance, you can define a class called Component that has a draw method on it. Other classes such as Button, Image and SelectBox will derive from Component and therefore inherit the draw method. Each of them can override the draw method to define their own behavior, but the framework treats all these types as instances of Component and calls draw on them. But Rust doesn't have inheritance, so we have to find another way out.
This is also the meaning of C++ dynamic polymorphism.

If a generic feature constraint is used, then the list must all be of the same type. So no

#![allow(unused)]
fn main() {
    
    
pub struct Screen<T: Draw> {
    
    
    pub components: Vec<T>,
}

impl<T> Screen<T>
    where T: Draw {
    
    
    pub fn run(&self) {
    
    
        for component in self.components.iter() {
    
    
            component.draw();
        }
    }
}
}

In order to solve all the above problems, Rust introduces a concept - characteristic object. Box implementation.

pub struct Screen { pub components: Vec<Box>, }


stores a dynamic array in which the element type is Draw characteristic object: Box. Any type that implements Draw characteristic can be stored in it.

Next, define the run method for Screen, which is used to render the UI components in the list on the screen:

impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }






Screen When running, we do not need to know the specific type of each component. It does not check whether the component is an instance of Button or SelectBox. As long as it implements the Draw feature, it can be packaged into a Box feature object through Box::new and then rendered on the screen.
The advantage of using trait objects and the Rust type system to do things like duck typing is that you don't have to check at runtime whether a value implements a specific method or worry about an error when you call it because the value doesn't implement the method.

The dynamic distribution of feature objects dyn emphasizes this "dynamic" feature.
When using trait objects, Rust must use dynamic dispatch. The compiler cannot know all the types that may be used in the trait object code, so it does not know which method implementation of which type should be called. To do this**, Rust uses pointers in the trait object at runtime to know which method needs to be called**The
Insert image description here
size of the trait object is not fixed: this is because, for the trait Draw, the type Button can implement the trait Draw, and the type SelectBox can also implement the trait Draw, so the feature does not have a fixed size.
The reference method of the feature object is almost always used, such as &dyn Draw, Box (Although the feature object does not have a fixed size, the size of its reference type is fixed, and it consists of two pointers (ptr and vptr), so it occupies two pointer sizes). One pointer ptr points to an instance of a specific type that
implements the characteristic Draw , that is, an instance of the type used as the characteristic Draw, such as an instance of the type Button or an instance of the type SelectBox; the other The pointer vptr points to a virtual table vtable, which stores instances of type Button or type SelectBox for callable methods implemented in the feature Draw.

Some commonly used traits
Next, we will also learn about several more important traits: traits
related to memory allocation and release, which
are used to distinguish different types, marking traits that assist the compiler in doing type safety checks, traits that
perform type conversion, and operator-related traits.
Debug/Display/Default.
Today we will first learn three traits related to memory

Memory related: Clone / Copy / Drop
The Clone trait has two methods:
clone()
clone_from() has a default implementation.

pub trait Clone {
    
    
  fn clone(&self) -> Self;

  fn clone_from(&mut self, source: &Self) {
    
    
    *self = source.clone()
  }
}

All traits define an implicit type Self, which refers to the type that currently implements this interface. ” ——Rust official documentation
Fill in the pit: self Self,&self,&Self
self
When self is used as the first parameter of a function, it is equivalent to self: Self. The &self parameter is equivalent to self: &Self Self
(equivalent to a Self in the generic)
method parameter is a kind of syntactic sugar, which is the receiving type of the method (for example, the type of impl in which this method is located).
It may appear in trait or impl. But often in trait, it is any The type that ultimately implements the trait is used instead (the type was not known when the trait was defined).

impl Clone for MyType {
    
    
    //我可以使用具体类型(当前已知的,例如MyType)
    fn clone(&self) -> MyType;

    //或者再次使用Self,毕竟Self更简短
    fn clone(&self) -> Self
}

Why use &self? Because it is a borrowing and does not transfer ownership. Otherwise, if the type enters the function and comes out, it cannot be used again.
In Rust, there is no implicit passing of this parameter to a method of a type. You have to explicitly pass the "current object" to the method parameter, so add self.
Then the difference between self as the first parameter and Self as the first parameter is. self is a method and Self is an associated function.
The calling method is different.

impl MyType{
    
    
    fn doit(&self, a: u32){
    
    
        //...
    }
    fn another(this: &Self, a: u32){
    
    
        //...
    }
}

fn main() {
    
    
    let m = Type;

    //都可以用作关联函数
    MyType::doit(&m, 1);
    MyType::another(&m, 2)

    //但只有”doit”可用作方法
    m.doit(3)    // m自动被借用
    m.another(4) //错误:没有命名为`another`的方法
}

So what is the difference between these 2?

// If a already exists
a = b.clone(); // The clone process will allocate memory.
a.clone_from(&b); // Avoid memory allocation and improve efficiency.
In these two sentences, if a already exists, memory will be allocated during the clone process, new memory will be used, and old memory will be released. Then use a.clone_from(&b) to avoid memory allocation and improve efficiency.
The Clone trait can be implemented directly through derived macros, which can simplify a lot of code. If each field in the struct data structure has implemented the Clone trait, you can use #[derive(Clone)].
Clone is a deep copy , where stack memory and heap memory are copied together.

Copy trait
The Copy trait has no additional methods. It is a marker trait (marker trait).
The code is defined as follows:
pub trait Copy: Clone {}
To implement Copy, you must implement the Clone trait. What is the use of a trait without any methods?
Although such a trait has no behavior, it can be used as a trait bound for type safety checking, so we call it a marked trait.
Copy refines Clone. A clone operation may be slow and expensive, but a copy operation is guaranteed to be fast and inexpensive, so copy is a faster clone operation. If a type implements Copy, the Clone implementation is irrelevant:

// 标注#[derive(Copy, Clone)]后 编译器自动生成的代码
impl<T: Copy> Clone for T {
    
    
    //clone 方法仅仅只是简单的按位拷贝
    fn clone(&self) -> Self {
    
    
        *self
    }
}

The Drop trait
code is defined as follows:
pub trait Drop { fn drop(&mut self); }

In most scenarios, there is no need to provide the Drop trait for the data structure. By default, the system will drop each field of the data structure in turn. But there are two situations where you may need to implement Drop manually.
I hope to do something when the data ends its life cycle, such as logging. (Somewhat similar to decoration mode)
Scenarios that require resource recycling. The compiler doesn't know what additional resources you use, so it can't help you drop them. For example, for the release of lock resources, Drop is implemented in MutexGuard to release lock resources:

impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    
    
    #[inline]
    fn drop(&mut self) {
    
    
        unsafe {
    
    
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}

Mark trait
Mark trait
Yesterday’s learning Copy trait is also a mark trait. Rust also supports some commonly used flag traits Size/Send/Sync/Unpin.
The tagged trait is a trait without a trait item. Their job is to "mark" implementation types as having certain properties that otherwise would have no way to be represented in the type system.

The Size trait is used to mark types with specific sizes. When using generic parameters, the Rust compiler will automatically add Sized constraints to the generic parameters. For example, the following two pieces of code have the same effect.
However, in some cases, T in the above code is a variable type, and then the type size is inconsistent. Rust provides ?Size to solve this problem.

Send / Sync are often implemented together

Let's look at the code definitions of Send and Sync first:
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}
These two traits are both unsafe auto traits. auto: means that the compiler will automatically add their implementation to the data structure when appropriate. unsafe: Indicates that the implemented trait may violate Rust's memory safety guidelines.
Send/Sync is the basis of Rust's concurrency safety:
if a type T implements the Send trait, it means that T can be safely moved from one thread to another, which means that ownership can be moved between threads.
If a type T implements the Sync trait, it means &T can be safely shared among multiple threads. A type T satisfies the Sync trait if and only if &T satisfies the Send trait.

The main data structures in the standard library that do not support Send/Sync are:
raw pointers *const T / *mut T. They are unsafe, so they are neither Send nor Sync.
UnsafeCell does not support Sync. In other words, any data structure using Cell or RefCell does not support Sync.
Reference counting Rc does not support Send or Sync. So Rc cannot cross threads.

(Fill in the pit: About the characteristics of cell, rc, and arc)

Operator conversion trait
In development, we often need to convert one type to another type.
Let’s first look at the comparison of these methods.

// 第一种方法,为每一种转换提供一个方法
// 把字符串 s 转换成 Path
let v = s.to_path();
// 把字符串 s 转换成 u64
let v = s.to_u64();

// 第二种方法,为 s 和要转换的类型之间实现一个 Into<T> trait
// v 的类型根据上下文得出
let v = s.into();
// 或者也可以显式地标注 v 的类型
let v: u64 = s.into();

Is this still useful? Obviously the second one is more friendly to coders like us, as we only need to remember one format. Different types of conversions implement a data conversion trait, so that the same method can be used to implement different types of conversions (a bit like generics?) This also symbolizes the opening and closing principle, which is open to expansion and closed to modification. The bottom layer can be extended to more data types. The original ones do not need to be modified, only new implementations are needed.

Following this idea, Rust provides two different sets of traits based on value types and reference types.
Value type: From / Into / TryFrom / TryInto
Reference type: AsRef / AsMut
pub trait From { fn from(T) -> Self; }

pub trait Into {
fn into(self) -> T;
}

// Implementing From will automatically implement Into
impl<T, U> Into for T where U: From { fn into(self) -> U { U::from(self) } } As you can see from the code, when implementing From Into will be automatically implemented when . In general, you only need to implement From. Both of these two methods can be used for type conversion. For example:






let s = String::from(“Hello world!”);
let s: String = “Hello world!”.into();
If your data type may cause errors during the conversion process, you can use TryFrom and TryInto. Their usage is the same as From/Into, except that there is an additional associated type Error in the trait, and the returned result is Result<T, Self::Error>.

AsRef and AsMut are used for conversion from reference to reference. Let’s look at their code definitions first:
pub trait AsRef where T: ?Sized { fn as_ref(&self) -> &T; }

pub trait AsMut where T: ?Sized { fn as_mut(&mut self) -> &mut T; } It can be seen from these two definitions that variable-size types of T are allowed, such as: str, [u8] and so on. In addition, except that AsMut is a variable reference, everything else is the same as AsRef, so we mainly look at AsRef



Operator related traits

deref trait
let mut x = 5;
{
    
    
    let y = &mut x;

    *y += 1
}

assert_eq!(6, x);

We use *y to access the data pointed to by the mutable reference y, not the mutable reference itself. Then you can modify its data, in this case add one to it.
References are not smart pointers, they just refer to a value, so the dereference operation is very straightforward. Smart pointers also store metadata about the pointer or data. When dereferencing a smart pointer, we only want the data, not the metadata, since dereferencing a regular reference only gives us the data and not the metadata. We want to be able to use smart pointers where regular references are used. To do this, you can overload the behavior of the * operator by implementing the Deref trait.
pub trait Deref { // Dereferenced result type type Target: ?Sized; fn deref(&self) -> &Self::Target; }



use std::ops::Deref;

struct Mp3 {
    
    
    audio: Vec<u8>,
    artist: Option<String>,
    title: Option<String>,
}

impl Deref for Mp3 {
    
    
    type Target = Vec<u8>;

    fn deref(&self) -> &Vec<u8> {
    
    
        &self.audio
    }
}

fn main() {
    
    
    let my_favorite_song = Mp3 {
    
    
        // we would read the actual audio data from an mp3 file
        audio: vec![1, 2, 3],
        artist: Some(String::from("Nirvana")),
        title: Some(String::from("Smells Like Teen Spirit")),
    };

    assert_eq!(vec![1, 2, 3], *my_favorite_song);
}

Here, the audio data is what we want, and the song title and author name are metadata and are not needed. So you need to implement the Deref trait to return audio data.

For more knowledge about traits, please refer to

https://juejin.cn/post/6957216834772795422

Guess you like

Origin blog.csdn.net/weixin_53344209/article/details/130011403