likes
comments
collection
share

Rust中错误处理是如何进行的?

作者站长头像
站长
· 阅读数 14

Rust中错误处理是如何进行的?

在其他语言中,对于错误的处理是通过“异常”这一操作进行统一处理。而在Rust中,对这两种错误提供了不同的解决方法:可恢复错误和不可恢复错误

Panic!和不可恢复的错误

Rust的错误处理有何不同?

Rust有极高的可靠性,这也延伸到了错误处理的领域:

  • 大部分情况下,我们在编译时被提示错误,并进行处理

Rust中的错误的分类:

  • 可恢复:例如文件未找到,会将错误信息返回给用户,并让其再次尝试寻找这个文件
  • 不可恢复:也称为bug,例如访问的索引超出范围

在其他语言中,没有对这两种错误进行区分,而是通过“异常”这一操作进行统一处理。而在Rust中,对这两种错误提供了不同的解决方法:

  • 可恢复错误:Result<T,E>
  • 不可恢复:panic!宏(程序会立即终止)

不可恢复的错误与panic!

当panic!宏执行,会经历以下步骤:

  • 你的程序会打印一个错误信息
  • 展开(unwind)、清理调用栈(Stack)
  • 退出程序

对于使用panic!,展开或中止(abort)调用栈

默认情况下,当panic发生:

  • 程序展开调用栈(工作量大):
    • Rust沿着调用栈往回走
    • 清理每个遇到的函数中的数据
  • 或者立即中止(abort)调用栈:
    • 不进行清理,直接停止程序
    • 内存需要OS进行清理

如果我们想让二进制文件更小,把设置从“展开”改为“中止”:

  • 在Cargo.toml中适当的profile部分设置为:panic='abort'
[package]
name = "panic"
version = "0.1.0"
edition = "2021"

[dependencies]

[profile.release]
panic = 'abort'

我们在src/main.rs中使用panic!宏:

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

使用cargo run运行,会输出:hread 'main' panicked at 'crash and burn', src\main.rs:2:5

使用panic!产生的回溯信息

我们再看一个例子:

let v = vec![1, 2, 3];
v[88];

运行会报错:thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 88', src\main.rs:5:5。

我们也可以更改RUST_BACKTRACE来得到更详细的信息

通过上面的例子,我们知道panic!可能出现在:

  • 我们写的代码中
  • 我们所依赖的代码中

我们可通过调用panic!的函数的回溯信息来定位引起问题的代码(通过设置环境变量RUST_BACKTRACE得到回溯信息)

Result枚举与可恢复的错误

Result枚举

在程序中,大部分错误都没严重到要中止程序的程度。对于这种错误,我们可以使用Result枚举来处理,下面是Result枚举的定义:

enum Result<T,E>{
    OK(T),
    Err(E),
}
  • T:操作成功的情况下,Ok变体里返回的数据的类型
  • E:操作失败的情况下,Err变体里返回的错误的类型

我们尝试打开一个文件,这个文件有可能不存在

let file = File::open("hello.text");//file: Result<File, Error>

处理Result的一种方式:match表达式

和Option枚举一样,Result及其变体也是由prelude带入作用域

use std::fs::File;

fn main() {
    let f = File::open("hello.text");
    match f {
        Ok(file) => file,
        Err(error) => panic!("{:?}", error),
    };
}

报错:thread 'main' panicked at 'Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:7:23

匹配不同的错误

use std::{fs::File, io::ErrorKind};

fn main() {
    let f = File::open("hello.text");
    match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.text") {
                Ok(file_create) => file_create,
                Err(e) => panic!("Error create a file:{:?}", e),
            },
            other_error => panic!("Error opening the file:{:?}", other_error),//other_error为自定义变量名
        },
    };
}

上面的例子中使用了很多的match,当然match很有用,但同时也很原始。

我们在后面可以使用闭包(closure)。Result<T,E>有很多方法:

  • 它们接收闭包作为参数
  • 使用match实现
  • 使用这些方法会让代码更简洁

下面的代码会在后面了解闭包后才能明白

use std::{fs::File, io::ErrorKind};

fn main() {
    let f = File::open("hello.text").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.text").unwrap_or_else(|error| {
                panic!("Error creating file:{:?}", error);
            })
        } else {
            panic!("Error opening file:{:?}", error);
        }
    });
}

unwrap

unwrap:match表达式的一个快捷方法:

  • 如果Result结果是Ok,返回Ok里面的值
  • 如果Result结果是Err,调用panic!宏
let f = File::open("hello.text").unwrap();

上面的代码等同于:

use std::fs::File;

fn main() {
    let f = File::open("hello.text");
    match f {
        Ok(file) => file,
        Err(error) => panic!("{:?}", error),
    };
}

但是这个方法有个缺点:不能自定义panic!的内容

所以rust还给我们提供了expect方法

expect

expect:和unwrap类似,但可指定错误信息

let f = File::open("hello.text").expect("无法打开文件:hello.txt");

thread 'main' panicked at '无法打开文件:hello.txt: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:4:38

传播错误

之前我们介绍的是在函数中处理错误,现在我们要将错误返回给调用者

根据前面的例子,我们可能会写出这样的代码:

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

fn read_file() -> Result<String, io::Error> {
    let f = File::create("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    //返回match
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

fn main() {
    let result = read_file(); //result: Result<String, Error>
}

在Rust中,传播错误是十分常见的。所以说它还提供了?运算符

?运算符

?运算符是一种传播错误的一种快捷方式

我们使用运算符来简化上面的例子:

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

fn read_file() -> Result<String, io::Error> {
    let mut f = File::create("hello.txt")?;

    let mut s = String::new();
    f.read_to_string(&mut s);
    Ok(s)
}

fn main() {
    let result = read_file(); //result: Result<String, Error>
}

如果Result是Ok:Ok中的值就是表达式的结果,然后继续执行程序

如果Result是Err:Err就是整个函数的返回值,就像使用了return

?与from函数

Trait std::convert::From上的from函数:

  • 用于错误之间的转换

?所应用的错误,会隐式的被from函数处理,当?调用from函数时:它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型

我们还可以使用链式调用来优化:

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

?运算符只能用于返回Result的函数

?运算符与main函数

main函数的返回类型为:()

但它的返回类型也可以是:Result<T,E>

use std::{error::Error, fs::File};

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

其中,Box<dyn Error>是trait对象,我们可以简单理解为:“任何可能的错误类型”