English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Rust has a unique mechanism for handling exceptional situations, which is not as simple as the try mechanism in other languages.
Firstly, there are generally two types of errors in the program: recoverable errors and unrecoverable errors.
A typical case of a recoverable error is a file access error. If accessing a file fails, it may be because it is being used, which is normal, and we can solve it by waiting.
But there is also another type of error caused by logical errors that cannot be solved in programming, such as accessing a position outside the end of an array.
Most programming languages do not distinguish between these two types of errors and use the Exception (exception) class to represent errors. There is no Exception in Rust.
For recoverable errors, use the Result<T, E> type to handle them, and for unrecoverable errors, use the panic! macro to handle them.
This chapter has not introduced the syntax of Rust macros specifically, but we have already used the println! macro. Since these macros are relatively simple to use, there is no need to master them thoroughly at this stage. We can first learn how to use the panic! macro in the same way.
fn main() { panic!("error occurred"); println!("Hello, Rust"); }
Running Result:
thread 'main' panicked at 'error occurred', src\main.rs:3:5 note: run with `RUST_BACKTRACE=1`environment variable to display a backtrace.
Clearly, the program cannot run as expected to println!("Hello, Rust") but stops running when the panic! macro is called.
Unrecoverable errors will inevitably lead to the program being struck down and terminated.
Let's focus on the two lines of error output:
The first line outputs the location of the panic! macro call and the error information it outputs.
The second line is a prompt, which translates to Chinese as "Through `RUST_BACKTRACE="1` Environment variable run to display backtrace". Next, we will introduce backtrace (backtrace).
Following the previous example, we create a new terminal in VSCode:
Set the environment variable in the newly created terminal (the method of different terminals is different, here are two main methods):
If on Windows 7 And in Windows system versions above, the default terminal command line is Powershell, please use the following command:
$env:RUST_BACKTRACE=1 ; cargo run
If you are using a UNIX system such as Linux or macOS, the default command line is usually bash, please use the following command:
RUST_BACKTRACE=1 cargo run
Then, you will see the following text:
thread 'main' panicked at 'error occurred', src\main.rs:3:5 stack backtrace: ... 11: greeting::main at .\src\main.rs:3 ...
Backtracing is another way to handle unrecoverable errors, which will expand the running stack and output all information, and then the program will still exit. The ellipsis (...) here omits a large amount of output information, and we can find the error triggered by the panic! macro we wrote.
This concept is very similar to exceptions in the Java programming language. In fact, in C, we often set the return value of a function to an integer to express the error encountered by the function, and in Rust, we express exceptions through the Result<T, E> enumeration class as the return value:
enum Result<T, E> { Ok(T), Err(E), }
In the Rust standard library, the return values of functions that may throw exceptions are all of the Result type. For example: when we try to open a file:
use std::fs::File; fn main() { let f = File::open("hello.txt"); match f { Ok(file) => { println!("File opened successfully."); }, Err(err) => { println!("Failed to open the file."); } } }
If the hello.txt file does not exist, it will print "Failed to open the file.".
Of course, the if let syntax we discussed in the enumeration chapter can simplify the match syntax block:
use std::fs::File; fn main() { let f = File::open("hello.txt"); if let Ok(file) = f { println!("File opened successfully."); } else { println!("Failed to open the file."); } }
If you want to treat a recoverable error as an unrecoverable error, the Result class provides two methods: unwrap() and expect(message: &str) :
use std::fs::File; fn main() { let f1 = File::open("hello.txt").unwrap(); let f2 = File::open("hello.txt").expect("Failed to open."); }
This program is equivalent to calling the panic! macro when Result is Err. The difference is that expect can send a specified error message to the panic! macro.
What we have discussed before is the way to handle errors received, but what if we want to pass errors out when we write our own function and encounter errors?
fn f(i: i32) -> Result<i32, bool> { if i >= 0 { Ok(i) } else { Err(false) } } fn main() { let r = f(10000); if let Ok(v) = r { println!("Ok: f(-1) = {}", v); } else { println!("Err"); } }
Running Result:
Ok: f(-1) = {}", v); 10000
This program is the source of the error, and now we will write a function g that passes errors:
fn g(i: i32) -> Result<i32, bool> { let t = f(i); return match t { Ok(i) => Ok(i), Err(b) => Err(b) }; }
Function g passes the errors that function f may encounter (here g is just a simple example, and the function that passes errors generally includes many other operations).
Writing it this way is somewhat verbose, in Rust, you can add the ? operator after the Result object to pass the same type of Err directly out:
fn f(i: i32) -> Result<i32, bool> { if i >= 0 { Ok(i) } else { Err(false) } } fn g(i: i32) -> Result<i32, bool> { let t = f(i)?; Ok(t) // Since it is determined that t is not Err, t is already i here32 Type } fn main() { let r = g(10000); if let Ok(v) = r { println!("Ok: g(10000) = {}", v); } else { println!("Err"); } }
Running Result:
Ok: g(10000) = {} 10000
? Symbol's actual function is to directly extract the non-exceptional values of the Result class, and if there is an exception, it returns the Result with the exception. Therefore, the ? symbol is only used for functions with return value type of Result<T, E>, where the E type must be consistent with the E type of the Result handled by?.
Up to now, it seems that Rust does not have a syntax like the try block that can directly resolve all similar exceptions that occur at any location, but this does not mean that Rust cannot implement it: we can completely implement the try block in an independent function, and pass all exceptions out to be resolved. In fact, this is a good programming method for a well-divided program: it should pay attention to the integrity of independent functions.
But this requires judgment of the Result's Err type, and the function to get the Err type is kind().
use std::io; use std::io::Read; use std::fs::File; fn read_text_from_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path)?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } fn main() { let str_file = read_text_from_file("hello.txt"); match str_file { Ok(s) => println!("{}", s), Err(e) => { match e.kind() { io::ErrorKind::NotFound => { println!("No such file"); }, _ => { println!("Cannot read the file"); } } } } }
Running Result:
No such file