Rust language Bible 31 - return values Result and?

Posted by devans on Mon, 20 Dec 2021 10:47:56 +0100

This article is excerpted from < < rust language Bible > > A Book
Welcome to Rust programming school to learn and exchange:
QQ group: 1009730433

Recoverable error Result

Remember the thinking about file reading mentioned in the previous section? At that time, we solved how to deal with unrecoverable errors during reading. Now let's see how to deal with normal return and recoverable errors during reading.

Suppose we have a message server. Each user connects to the server through websocket to receive and send messages. This process involves the reading and writing of socket files. At this time, if a user makes an error in reading and writing, it is obvious that it cannot directly panic. Otherwise, the server will crash directly and all users will be disconnected, Therefore, we need a milder error handling method: result < T, E >

As mentioned in the previous chapter, result < T, E > is an enumeration type, which is defined as follows:

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

The generic parameter T represents the correct value stored when successful. The storage method is Ok(T). E represents the error value stored. The storage method is Err(E). The boring explanation is never as vivid and accurate as the code. Therefore, let's take a look at the following example of opening a file:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

If the above File::open returns a Result type, the problem arises:

How do I know the variable type or the return type of a function

There are several common ways:

  • The first method is to query standard library or third-party library documents, search File, and then find its open method, but the second method is recommended here:
  • stay Rust IDE In chapter, we recommend VSCode IED and trust analyze plug-ins. If you successfully install them, you can easily view the code by code jump in VScode. At the same time, the trust analyze plug-in also marks the types in the code, which is very convenient and easy to use!
  • You can also try to mark a wrong type deliberately, and then let the compiler tell you:
let f: u32 = File::open("hello.txt");

The error prompt is as follows:

error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |                  ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
  |
  = note: expected type `u32`
             found type `std::result::Result<std::fs::File, std::io::Error>`

The above code deliberately marks the f type as an integer, and the compiler is not happy immediately. Are you fooling me? Open file operation returns an integer? Come on, big brother, let me tell you what to return: STD:: result:: result < STD:: FS:: file, STD:: IO:: error >, my God, what a long type!

Don't panic. It's actually very simple. First, the Result itself is defined in std::result, but because the Result is very common, it is included in prelude in (the commonly used results are introduced into the current scope in advance). Therefore, there is no need to manually import std::result::Result, so the return type can be simplified to result < std::fs::File, std::io::Error >. Do you see if it is very similar to the standard result < T, E > enumeration definition? But t is replaced with a specific type std::fs::File, which is a file handle type, and E is replaced with std::io::Error , is an IO error type

This return value type indicates that if the File::open call succeeds, it returns a file handle that can be read and written. If it fails, it returns an IO error: the file does not exist or does not have permission to access the file. In short, File::open needs a way to tell the caller whether it succeeds or fails, and return specific file handle (success) or error information (failure). Fortunately, the Result enumeration of these information can provide:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}

The code is very clear. Match the result < T, E > type after opening the file. If it is successful, assign the file handle file stored in Ok(file) to f. if it fails, throw out the error message error stored in Err(error) using panic, and then end the program, which is very consistent with the use scenario of panic mentioned above.

Well, it's not that reasonable:)

Handle the returned error

Direct panic is still too rude, because there are many kinds of IO errors. We need to deal with some errors in a special way, instead of all errors crashing directly:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    };
}

After matching the error, the above code performs a detailed matching analysis on the error. The final result is:

  • If there is no error ErrorKind::NotFound in the file, the file is created. Here, the File::create also returns Result, so continue to process it with match: the creation is successful, assign the new file handle to f, and if it fails, panic
  • The remaining errors are panic

Although it is very clear, the code is still a little wordy. We will Simplified error handling The chapter focuses on how to write more elegant mistakes

panic: unwrap and expect

I have seen the brief introduction of the two brothers in the previous section. Let's review it here.

When we don't need to deal with error scenarios, such as writing prototypes and examples, we don't want to use match to match the result < T, E > to obtain the T value. Because of the exhaustive matching feature of match, you always have to deal with the Err branch. Is there any way to simplify this process? Yes, the answers are unwrap and expect.

Their function is to take out the value in Ok(T) if it returns success. If it fails, it will panic directly. Real warriors will never have more BB and crash directly

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

If hello. Net does not exist when this code is called Txt file, unwrap will directly panic:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

expect is similar to unwrap, except it allows you to specify panic! Error message at:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

Errors are reported as follows

thread 'main' panicked at 'Failed to open hello.txt: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

It can be seen that expect can provide more accurate error information than unwrap, and it will be more practical in some scenarios.

Propagation error

It is almost impossible for our program to have only function calls in the form of a - > B. It is possible for a well-designed program and a function call involving more than ten layers. Error handling is often not handled where the call goes wrong. In practical application, the probability is that errors will be uploaded layer by layer and then handed over to the upstream function of the call chain for processing. Therefore, error propagation will be very common

For example, the following function reads the user name from the file and returns the result:

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    // Open the file, f is' result < file handle, IO:: error >`
    let f = File::open("hello.txt");

    let mut f = match f {
        // Open the file successfully. Assign the file handle to f
        Ok(file) => file,
        // Failed to open the file, return the error (propagate up)
        Err(e) => return Err(e),
    };
    // Create dynamic string s
    let mut s = String::new();
    // Read data from f file handle and write to s
    match f.read_to_string(&mut s) {
        // After reading successfully, the string encapsulated by Ok is returned
        Ok(_) => Ok(s),
        // Propagate errors upward
        Err(e) => Err(e),
    }
}

There are several points worth noting:

  • This function returns a result < string, IO:: error > type. When the user name is read successfully, it returns Ok(String), and when it fails, it returns Err(io:Error)
  • File::open and f.read_to_string returned result < T, E > in E > is io::Error

It can be seen that the function propagates the io::Error error upward, and the caller of the function will eventually reprocess the result < string, io::Error >. 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 directly panic, or wrap the specific error reason and write it into the socket to present it to the end user.

But the above code also has its own problem, that is, it is too long (excellent programmers have many advantages, and the biggest advantage is laziness). I think it is a little excellent, so I can't see such verbose code. Let's talk about how to simplify it.

Big star in the communication circle:?

When a big star comes out, you must have a row. Come and have a look? Arrangement of:

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

See, this is the layout. Compared with the previous match function, the code is directly reduced by more than half. However, one mountain is more difficult than another. I can't understand it!

Actually? A macro, which as like as two peas in match:

let mut f = match f {
    // Open the file successfully. Assign the file handle to f
    Ok(file) => file,
    // Failed to open the file, return the error (propagate up)
    Err(e) => return Err(e),
};

If the result is Ok(T), assign t to f. if the result is Err(E), this error is returned, so? It is especially suitable for spreading errors.

Although? Consistent with match function, but in fact? Will be better. What's the solution?

Imagine that a well-designed system must have user-defined error features, and there is likely to be a hierarchical relationship between errors, such as std::io::Error and std::error::Error in the standard library. The former is the io related error structure, the latter is the most common standard error feature, and the former implements the latter, Therefore, std::io::Error can be converted to std:error::Error.

Understand the above error conversion,? It is easy to understand that it can automatically carry out type promotion:

fn open_file() -> Result<File, Box<dyn std::error::Error>> {
    let mut f = File::open("hello.txt")?;
    Ok(f)
}

In the above code, the error returned when File::open reports an error is std::io::Error, but open_ The error type returned by the file function is the characteristic object of std::error::Error. You can see an error type through? After returning, it becomes another error type, which is? The magic of.

The root cause is the from feature defined in the standard library. This feature has a method from, which is used to convert one type to another,? This method can be called automatically, followed by implicit type conversion. Therefore, as long as the error ReturnError returned by the function implements the from < OtherError > feature, then? It will automatically convert OtherError to ReturnError.

This conversion is very easy to use, which means that you can use a large and complete ReturnError to cover all error types. You only need to implement this conversion for each sub error type.

One yard is shorter than another:

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}

See? It can also realize chain call. File::open returns when there is an error. If there is no error, it will take out the value in Ok for the next method call. It's wonderful. I'm ecstatic from the Go language (in fact, I won't tell you the pain and pain of learning Rust).

Not only stronger, but also the strongest. I don't believe there are people shorter than me ((don't misunderstand):

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    // read_to_string is a method defined in std::io, so it needs to be referenced above
    fs::read_to_string("hello.txt")
}

Reading data from a file to a string is a common operation, so the Rust standard library provides us with fs::read_to_string function, which will open a file, create a string, read the file content, and finally write a string and return. Because this function can't help us learn the content of this chapter, I just want to shock you at the end:)

? Return for Option

? It can be used not only for the propagation of Result, but also for the propagation of Option. Recall the definition of Option:

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

Result passed? If an error is returned, the Option will pass? Return to None:

fn first(arr: &[i32]) -> Option<&i32> {
   let v = arr.get(0)?;
   Some(v)
}

In the above function, arr.get returns an option < & I32 > type because? If the result of get is None, return None directly. If it is some (&i32), assign the value inside to v.

In fact, this function is a bit of icing on the cake. We can write a simpler version:

fn first(arr: &[i32]) -> Option<&i32> {
   arr.get(0)
}

What do you say? If there is no demand, manufacturing demand should also go up... Don't learn from me. This is a big taboo in software development You can only wash your eyes with code:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

The above code shows how to use the? Usage of returning None in advance The next method returns the Option type: if it returns some (& STR), it will continue to call the chars method. If it returns None, it will directly return None from the whole function without continuing the chain call

Novice? Common mistakes

Beginners in use? You always make mistakes, such as writing such code:

fn first(arr: &[i32]) -> Option<&i32> {
   arr.get(0)?
}

This code cannot be compiled. Remember:? The operator needs a variable to carry the correct value. Only the wrong value can be returned directly, not the correct value. So? Can only be used in the following forms:

  • let v = xxx()?;
  • xxx()?.yyy()?;

main function with return value

Because just now? You can easily see that this code can't be compiled due to usage restrictions:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

Because? The return value in the form of result < T, E > is required, and the return of the main function is (), so it cannot be satisfied. Is there no solution?

In fact, Rust also supports another form of main function:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

So you can use it? Back in advance, and we saw the Box<dyn Error> feature object again, because std::error:Error is the highest level of error in Rust. All the errors in other standard libraries achieve this feature, so we can use this feature object to represent all errors, even if any standard library function is invoked in the main function. Can be returned through the feature object box < dyn error >

As for the main function, it can have multiple return values because it implements the [std::process::Termination] feature. So far, this feature has not entered the stable version of Rust. Maybe you can implement this feature for your own type in the future!

So far, the basic content learning of Rust has been completed. Next, we will learn the advanced content of Rust and officially open your master's road.

Topics: Back-end Rust