Rust的错误处理

Rust的错误主要分可恢复(recoverable)和不可恢复(unrecoverable)两种,对于可恢复错误,只用报告问题并重试操作,而对于不可恢复错误则需要立即停止程序。可恢复错误通过Result类型来处理,不可恢复错误通过panic!宏来处理。

用panic!处理不可恢复的错误🔗

执行会造成代码panic的操作(越界访问数组)或调用panic!宏,都会造成程序panic,这是panic会打印错误信息,展开并清理栈数据然后退出。通过环境变量可以让Rust在panic时打印调用堆栈(call stack)以便定位panic的原因。

ℹ️ 注意

panic时也可选择直接终止(abort),不清理数据就退出,内存需要由操作系统清理。在Cargo.toml的[profile]添加panic = 'abort'即可,例如要在release模式下panic时直接终止,则:

[profile.release]
panic = 'abort'

直接调用panic!宏:

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

运行结果:

thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这里可以看到panic的位置,也告诉我们可以使用RUST_BACKTRACE=1环境变量来显示backtrace。

使用panic!宏的backtrace🔗

将RUST_BACKTRACE环境变量设置为任何不是0的值来获取backtrace,输入RUST_BACKTRACE=1 cargo run运行程序。
需要获取带有带有这些信息的backtrace,需要启用debug标识,而在不使用--release标识编译时,debug标识默认是开启的。

用Result处理可恢复的错误🔗

Result类型表示可能成功(Ok)或失败(Err)的操作,Result类型是枚举,定义如下:

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

考虑下面代码:

use std::fs::File;

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

File::open的返回值是Result<T, E>,当File::open成功时,greeting_file_result变量的值是Ok,其中Ok的值是File类型的文件句柄;当File::open失败时,greeting_file_result的值是Err,其中Err的值是std::io::Error类型的错误对象。

若要实现文件读取操作,考虑下面代码:

use std::fs::File;

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

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

这里如果File::open成功,则greeting_file的值是File类型的文件句柄,否则程序会panic并打印错误信息,告诉我们哪里出错了。

匹配不同错误🔗

上述代码无论什么错误都会调用panic!宏,但有时我们希望对不同的错误采取不同的处理方式,考虑如下代码:

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

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

    let greeting_file = match greeting_file_result {
        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),
        },
    }
}

这里如果File::open失败,则判断错误类型,如果错误类型是ErrorKind::NotFound,则尝试创建文件,如果创建文件也失败,则调用panic!宏,否则返回创建的文件句柄;如果错误类型不是ErrorKind::NotFound,则调用panic!宏。

ℹ️ 注意

处理Result类型时,除了match语句外,使用闭包(closure)会使代码更加简洁。

失败时panic的简写:unwarp和expect🔗

Result类型定义了unwarp方法,如果Result是Ok,则返回Ok中的值,否则调用panic!宏。expect方法与unwarp方法类似,但expect方法接受一个错误信息参数,当Result是Err时,expect方法会打印错误信息并调用panic!宏。

传播错误🔗

考虑下面代码:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt")?;
    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut username = String::new();
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

这里如果成功,则返回OK中的值,否则返回同样的Err,这就是传播错误。

用?运算符传播错误🔗

考虑下面代码:

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

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

在Result后加上?运算符,如果Result是Ok,则返回Ok中的值,否则返回Err,与上面的match表达式一致。
与match不用的是,?会将错误值传递给from函数,它定义于标准库的From trait中,用于将错误类型从一种转换为另一种,在有自定义的错误类型时可以节省代码。

在?之后使用链式方法调用进一步缩短代码:

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

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

可见确实节省了很多代码。

下面还有更为简短的写法:

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

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

fs::read_to_string函数会打开文件,读取文件内容,然后将内容放入String中,并返回Result类型。

⚠️ 警告

?运算符只能用于返回Result或Option类型的函数,如果函数返回的是其他类型,则不能使用?运算符。
一般main函数返回(),修改后的main函数也可以使用?:

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

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

总结🔗

本文讲解了Rust的错误处理,介绍了panic!宏和Result类型,并介绍了如何使用?运算符传播错误。