Collection containers (slicing and hashing) and error handling in rust

String, array [T:n], list Vec\hash table HashMap<K,V>, etc.
Slice;
circular buffer VecDeque, two-way list LinkedList, etc. (Does this refer to a doubly linked list?)
What these collection containers have in common:
they can be traversed and
map-reduce operations can be performed.
Can be converted from one type to another.

Mainly learn about slicing and hashing

Slice
definition: It is a group of data structures of the same type but of uncertain length that are continuously stored in memory .

fn main() {
    
    
    let arr = [1, 2, 3, 4, 5];
    let vec = vec![1, 2, 3, 4, 5];
    let s1 = &arr[..2];
    let s2 = &vec[..2];
    println!("s1: {:?}, s2: {:?}", s1, s2);

    // &[T]&[T] 是否相等取决于长度和内容是否相等
    assert_eq!(s1, s2);
    // &[T] 可以和 Vec<T>/[T;n] 比较,也会看长度和内容
    assert_eq!(&arr[..], vec);
    assert_eq!(&vec[..], arr);
}

Insert image description here
Insert image description here
As can be seen from the above, slicing is usually in the form of a reference. In fact, the pointer points to the actual data space, and the reference of vec is a pointer to the pointer.

String is a special Vec, so slicing on String is also a special structure &str.
Insert image description here
A direct reference to string is a pointer. If it is &str or sliced, it is a fat pointer.

Iterators can be said to be the twin brothers of slices. **Slices are views of collection data, and iterators define various access operations to collection data. **iterator trait has many methods, but generally we only need to define its associated type Item and next() method.
Item defines the data type taken out from the iterator each time.
next() is a method to get the next value. For None, it means there is no more.

Slicing abstracts them into the same access method, achieving the same abstraction on different data structures . This method is worth learning.
Hash tables
are similar to the method of creating dynamic arrays Vec. You can use the new method to create a HashMap, and then Insert key-value pairs through the insert method.
Created using iterators and collect methods.
In actual use, not all scenarios can create a new hash table and then leisurely insert the corresponding key-value pairs in sequence. Instead, it may be obtained from another data structure. The corresponding data is finally generated as a HashMap.
For example, consider a scenario where there is a table that records the names and points of each team in the football league. If this table is imported into a Rust project, a reasonable data structure is the Vec<(String, u32)> type. The elements in the array are tuples. This data structure is very suitable for table data: the data in the table is stored row by row, and each row stores a piece of information (team name, points).
But in many cases, it is necessary to query the corresponding points through the team name. At this time, the dynamic array is not applicable, so HashMap can be used to save the relevant team name -> points mapping relationship. The ideal is very full, but the reality is very skinny. How to quickly write the data in Vec<(String, u32)> into HashMap<String, u32>?
Iterating through the list and inserting each tuple into the HashMap as a KV pair is very simple, but... it doesn't look very smart, in other words - not rusty enough.
Fortunately, Rust provides us with a very elegant solution: first convert the Vec into an iterator, and then use the collect method to collect the elements in the iterator and convert it into a HashMap:

fn main() {
    
    
    use std::collections::HashMap;

    let teams_list = vec![
        ("中国队".to_string(), 100),
        ("美国队".to_string(), 10),
        ("日本队".to_string(), 50),
    ];

    let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
获取哈希表元素时

The get method returns an Option<&i32> type: when the query cannot be found, it will return None, and when the query is found, it will return Some(&i32)
&i32 is a borrowing of the value in the HashMap. If borrowing is not used, ownership transfer may occur.

Error Handling
Three ways to handle errors: using return values, exception handling, and the type system. The current solution for Rust is to mainly use the type system to handle errors, supplemented by exceptions to deal with unrecoverable errors.

In C language, return values ​​(error codes) are basically used to handle errors. For example, if fopen(filename) fails to open a file, it returns NULL, if it fails to create a socket, it returns -1, etc. This has many limitations. The return value has its own semantics. Confusing errors and return values ​​** increases the mental burden on developers. **It is very likely that the developer will forget to handle it, or misremember the semantics of the return value.

Exception handling is used in C++ and JAVA to handle errors. C++ exception handling involves three keywords: try, catch, and throw.
If a block throws an exception, the method to catch the exception will use the try and catch keywords. Code that may throw exceptions is placed in the try block. The code in the try block is called protection code.
You can use the throw statement to throw an exception anywhere in a block of code. The operand of the throw statement can be any expression, and the type of the result of the expression determines the type of exception thrown.
The following is an example of an exception thrown when trying to divide by zero:
double division(int a, int b)
{ if( b == 0 ) { throw “Division by zero condition!”; } return (a/b); }





int main ()
{ int x = 50; int y = 0; double z = 0; try { z = division(x, y); cout << z << endl; }catch (const char msg) { cerr << msg << endl; } * return 0; } Disadvantages of exception handling: First, the program flow of large exception handling is very complicated, and the function return point may be unexpected, which makes code management and debugging difficult. , so the first principle to ensure exception safety is: avoid throwing exceptions. Another serious problem with exception handling is that developers abuse exceptions. The overhead of exception handling is much greater than that of handling return values, and misuse will have a lot of additional overhead .












Rust summarizes the experience of its predecessors and uses the type system to build the main error handling process . The Option type and Result type are constructed.
pub enum Option { None, Some(T), } #[must_use = “this may be an variant, which should be handled”] pub enum Result<T, E> { Ok(T), Err(E), } Option It is a simple enum that can handle the simplest error type of value/no value.



ResultErr




Result is a more complex enum. When the function makes an error, it can return Err(E), otherwise Ok(T).
You can see that the Result type has must_use. If it is not used, a warning will be reported to ensure that the error is handled.
Insert image description here
In the example in the picture above, if we do not process the return value of read_file, prompts will begin. (Isn’t this back to Golang’s situation where if err != nil is everywhere?) One of the things that people criticize about the Go language is the extensive use of if err != nil {}, which lacks the beauty of some programming.

?Operator
If the execution propagation error does not want to be handled at the time, use the ?operator. This makes error propagation comparable to exception handling, while avoiding the problems caused by exception handling. (For a well-designed program, a function may involve more than ten layers of function calls. And error handling is often not handled where the call error occurs. In actual applications, there is a high probability that the error will be uploaded layer by layer and then handed over to the program. Calling functions upstream in the chain for processing, error propagation will be extremely common.)


#![allow(unused)]
fn main() {
    
    
use std::fs::File;
use std::io::{
    
    self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    
    
    // 打开文件,f是`Result<文件句柄,io::Error>`
    let f = File::open("hello.txt");

    let mut f = match f {
    
    
        // 打开文件成功,将file句柄赋值给f
        Ok(file) => file,
        // 打开文件失败,将错误返回(向上传播)
        Err(e) => return Err(e),
    };
    // 创建动态字符串s
    let mut s = String::new();
    // 从f文件句柄读取数据并写入s中
    match f.read_to_string(&mut s) {
    
    
        // 读取成功,返回Ok封装的字符串
        Ok(_) => Ok(s),
        // 将错误向上传播
        Err(e) => Err(e),
    }
}
}

In this example, the error is propagated to the place where the function is called, and it does not care how to deal with it. As for how to deal with it, it is the caller's business. If it is an error, it can choose to continue to propagate the error upward, or it can directly panic, or change the specific The error cause is packaged and written to the socket before being presented to the end user.
But the above code also has its own problems, that is, it is too long.
When a star appears, there must be a ranking. Let’s take a look at the ranking of ?:

#![allow(unused)]
fn main() {
    
    
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    
    
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

In fact? is a macro. Its function is almost exactly the same as the match above:
if the result is Ok(T), then T is assigned to f. If the result is Err(E), the error is returned, so ? is particularly suitable for use. Propagate errors.

Errors in Rust are mainly divided into two categories:
recoverable errors, which are usually used for errors that are acceptable from a global perspective of the system, such as errors in processing user access, operations, etc. These errors will only affect a user's own operation process. , without affecting the global stability of the system.
Unrecoverable errors, on the contrary, are usually global or systemic errors, such as array out-of-bounds access, errors that affect the startup process during system startup, etc. These errors The impact of errors is often fatal to the system
. Many programming languages ​​do not distinguish between these errors, but directly use exceptions to handle them. Rust has no exceptions, but Rust also has its own exceptions: Result<T, E> for recoverable errors, and panic! for unrecoverable errors.

There are two ways to trigger panic in Rust: passive triggering and actively calling
fn main() { let v = vec![1, 2, 3];

v[99];

} Array access out of bounds will trigger a passive panic

In some special scenarios, developers want to actively throw an exception, such as the failure to read a file during the system startup phase mentioned at the beginning.
In this regard, Rust provides us with the panic! macro. When this macro is called and executed, the program will print out an error message, expand the function call stack preceding the error point, and finally exit the program.
First, let’s call panic!. The simplest code implementation is used here. In fact, you can call it like this anywhere in the program:

fn main() {
panic!(“crash and burn”);
}

When panic! occurs, the program provides two ways to handle the termination process: stack expansion and direct termination.

Among them, the default method is stack expansion, which means that Rust will backtrace the data and function calls on the stack, so it also means more aftermath work. The advantage is that it can provide sufficient error information and stack call information to facilitate subsequent problems. Review . Direct termination, as the name suggests, exits the program directly without cleaning the data, leaving the aftermath to the operating system.
For the vast majority of users, it is best to use the default selection, but when you are concerned about the size of the final compiled binary executable file, you can try to use the direct termination method, such as the following configuration to modify the Cargo.toml file to achieve If panic is encountered in release mode , it will be terminated directly:
[profile.release]
panic = 'abort'

Will the program terminate after the thread panics?
To make a long story short, if it is the main thread, the program will terminate. If it is another child thread, the thread will terminate, but it will not affect the main thread. Therefore, try not to do too many tasks in the main thread, and leave these tasks to the sub-threads. Even if the sub-thread panics, it will not cause the entire program to end.

Finally, don’t use panic randomly. Used when recovery is not possible.
At the same time, I also experienced the idea and simplicity of macro programming here. In fact, panic does a lot of work, but it is included in the macro, making it very convenient to write code.

Summary:
Compared with C/Golang’s error handling method of directly using return values, Rust is more complete in terms of types and builds Option type and Result type with more rigorous logic, which not only prevents errors from being inadvertently ignored , but also avoids using verbose words. The expression is passed incorrectly;

Compared with the way C++/Java uses exceptions, Rust distinguishes between recoverable errors and unrecoverable errors, and uses Option/Result and panic!/catch_unwind respectively to deal with them, which is safer and more efficient and avoids many problems caused by exception safety;

Compared with its teacher Haskell, Rust's error handling is more practical and concise, thanks to its powerful metaprogramming function. Use? operator to simplify error propagation.

Guess you like

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