18_rust的错误处理

错误处理

不可恢复的错误与panic!宏

rust语言的错误处理:

  • rust语言具有较高的可靠性,有完备的错误处理机制,大部分情况下,能在编译是提示错误,并处理完错误。
  • rust没有类似异常处理的机制

错误的分类:

  • 可恢复错误:使用Result<T, E>机制,如文件未找到,可再次尝试。
  • 不可恢复:bug,使用panic!宏处理,当执行该宏时,程序立即终止,如索引越界访问。

当panic!宏执行时:

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

为应对panic,展开或中止(abort)调用栈。
默认情况下,当panic发生时,会执行:

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

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

  • 在Cargo.toml中适当的profile部分设置:panic = 'abort'
[profile.release]
panic = 'abort'

使用panic!产生的回溯信息

  • panic!可能出现在自己开发的代码和依赖的代码中,可通过panic!函数回溯信息来定位引起问题的代码。
  • 可通过设置环境变量RUST_BACKTRACE获得回溯信息。
  • 为了获取带有调试信息的回溯,必须启用调试符号(执行Cargo run命令时不带 --release)。
fn main() {
    panic!("panic test");
}
/* 运行结果:
thread 'main' panicked at 'panic test', src\main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\hello_cargo.exe` (exit code: 101)
*/

另一个例子:

fn main() {
    let v = vec![1, 2];
    v[5];
}
/* 运行结果:
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 5', src\main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
*/

根据提示,如果设置RUST_BACKTRACE=1环境变量就可显示backtrace,就可看到真正发生panic的地方了

>set RUST_BACKTRACE=1 && cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target\debug\hello_cargo.exe`
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 5', src\main.rs:3:5
stack backtrace:
   0: std::panicking::begin_panic_handler
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library\std\src\panicking.rs:593
   1: core::panicking::panic_fmt
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library\core\src\panicking.rs:67
   2: core::panicking::panic_bounds_check
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be/library\core\src\panicking.rs:162
   3: core::slice::index::impl$2::index<i32>
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be\library\core\src\slice\index.rs:261
   4: alloc::vec::impl$12::index<i32,usize,alloc::alloc::Global>
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be\library\alloc\src\vec\mod.rs:2675
   5: hello_cargo::main
             at .\src\main.rs:3
   6: core::ops::function::FnOnce::call_once<void (*)(),tuple$<> >
             at /rustc/5680fa18feaa87f3ff04063800aec256c3d4b4be\library\core\src\ops\function.rs:250
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
error: process didn't exit successfully: `target\debug\hello_cargo.exe` (exit code: 101)

可见第5个栈信息是测试代码,其他是库代码。提示显示可设置RUST_BACKTRACE=full可获得一详细的backtrace。

可恢复的错误与Result

通常情况下,会对程序运行可能的错误进行处理,而不是让程序终止运行,而且很多错误也是通过一定处理后便可继续运行,rust中采用Result枚举来处理错误。

Result枚举的定义

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

处理Result的一种方式:使用match表达式
和Option枚举一样,Result及其变体也是由prelude带入作用域的,所以使用OK和Err变体时不需要写成Result::OK的格式了。

use std::fs::File;
fn main() {
    let f = File::open("test.txt"); // 返回值就是个Result类型,std::result::Result<std::fs::File, std::io::Error>类型
    // 打开文件可能发生文件不存在的错误,如果成功则返回std::fs::File类型,不存在或无权限等导致不成功则返回io::Err错误
    let fp = match f {
        Ok(file) => file, // 如果成功则返回file实体,使得fp = file
        Err(error) => {
            panic!("Err msg: {:?}", error) // 这里可不用;号,加上也可
        },
    };
}
// 运行结果:thread 'main' panicked at 'Err msg: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:8:13

还可匹配不同的错误
如上述文件打开可能的错误是std::io::Error类型,也是一个struct,提供一错误类型方法kind(),返回值是一std::io::ErrorKind枚举,里边的变体用于描述可能发生IO错误,比如NotFound错误。

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

fn main() {
    let f = File::open("test.txt");
    let fp = match f {
        Ok(file) => file,
        Err(error) => match error.kind() { // 如果打开失败则创建文件
            ErrorKind::NotFound => match File::create("test.txt") { // 创建也可能失败,所以需要继续match
                Ok(fc) => fc,
                Err(e) => panic!("Err creat file: {:?}", e),
            },
            other_err => panic!("Err open file: {:?}", other_err),
        },
    };
}

运行结果是在项目目录下创建了一个文件test.txt。

上述代码中使用了很多match,代码很原始,可使用闭包(closure)。
Result<T, E>有很多方法:可接收闭包作为参数,使用match实现,可使上述代码更简洁,减少match嵌套。

use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let f = File::open("test.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("test.txt").unwrap_or_else(|error| {
                panic!("err create file: {:?}", error);
            })
        } else {
            panic!("Err open file: {:?}", error);
        }
    });
}

unwrap方法

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

  • 若Result结果是OK,返回Ok里面的值
  • 若Result结果是Err,则调用panic!宏
use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let f = File::open("test.txt");
    let fp = match f {
        Ok(file) => file,
        Err(error) => panic!("Err open file: {:?}", error),
    };
    // unwrap效果与前四行一样的
    let fp = File::open("test.txt").unwrap();
}

但错误信息不可自定义。为了解决这个问题,rust引入了expect方法。

expect方法

expect()方法:和unwrap类似,但可指定错误信息。

use std::fs::File;
use std::io::ErrorKind;
fn main() {
    let f = File::open("test.txt");
    let fp = match f {
        Ok(file) => file,
        Err(error) => panic!("Err open file: {:?}", error),
    };
    // expect效果与前四行一样的
    let fp = File::open("test.txt").expect("无法打开文件");
}

如果多个地方使用unwrap会打出一样的错误信息,需要手动排查是哪里打出来的,使用expect则可通过自定义信息区分。


传播错误

除了在函数内处理错误,还可将错误返回给调用者,传播错误,类似于其他语言的抛出异常。
用原来的方式实现错误返回:

use std::fs::File;
use std::io;
use std::io::Read;
fn io_err_propagation() -> Result<String, io::Error> {
    let f = File::open("test.txt");
    let mut fp = match f {
        Ok(file) => file,
        Err(e) => return Err(e), // 类型正好是IO error,可直接返回
    };
    let mut s = String::new();
    match fp.read_to_string(&mut s) {
        Ok(_) => Ok(s), // 如果读取成功则返回s
        Err(e) => return Err(e), // 如果失败也返回io err
    } // 最后一个表达式,没有分号,是返回值
}
fn main() {
    let s = io_err_propagation();
    println!("{:?}", s);
}

?运算符

?运算符:传播错误的一种快捷方式。
如对上面例子进行改造,代码简洁许多:

use std::fs::File;
use std::io;
use std::io::Read;
fn io_err_propagation() -> Result<String, io::Error> {
    let mut f = File::open("test2.txt")?; // 加?运算符后,作用与后面三行一致
    // 也是如果成功则将Ok的结果返回给f,如果失败则把错误信息作为return结果并执行return操作
    // let mut fp = match f {
    //     Ok(file) => file,
    //     Err(e) => return Err(e),
    // };
    let mut s = String::new();
    f.read_to_string(&mut s)?; // 作用与下三行一致,如果不成功会return Err(e)
    // let ret = match fp.read_to_string(&mut s) {
    //     Ok(_) => Ok(s),
    //     Err(e) => return Err(e),
    // }
    Ok(s) // 最后返回s
}
fn main() {
    let s = io_err_propagation();
    println!("{:?}", s);
}

?运算符的作用:

  • 如果Result是Ok:Ok中的值赋值作为表达式的结果,然后继续执行程序。
  • 如果Result是Err:Err就是整个函数的返回值,执行return操作(类似)。

?与from函数

Trait std::convert::From上的from函数,用于错误之间的转换。

  • 被?运算符所应用的错误,会隐式的被from函数处理。当?调用from函数时,它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型。
  • 用于针对不同错误原因,返回同一错误类型,前提是每个错误类型都实现了转换为所返回的错误类型的from函数。简单来说就是不是可被任意转换,比如IoErr错误要转成自定义错误ErrA,需要实现from函数,完成转换过程。

?运算符的链式调用

use std::fs::File;
use std::io;
use std::io::Read;
fn io_err_propagation() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("test.txt")?.read_to_string(&mut s)?; // 作用与原来一致
    Ok(s) // 最后返回s
}
fn main() {
    let s = io_err_propagation();
    println!("{:?}", s);
}

?运算符与main函数

注:?运算符只能用于返回Result的函数,比如:

use std::fs::File;
fn main() {
    let f = File::open("test.txt")?;
}
/*编译报错
 --> src\main.rs:5:35
  |
4 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
5 |     let f = File::open("test.txt")?;
  |                                   ^ cannot use the `?` operator in a function that returns `()`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
*/

通常main函数的返回类型是(),
main函数的返回类型也可是Result<T, E>。

use std::fs::File;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("test.txt")?;
    Ok(())
}

Box<dyn Error>是trait对象,简单理解为任何可能的错误类型。

什么时候使用panic!

总体原则:在定义一个可能失败的函数时,优先考虑返回Result,否则就使用panic!。
可使用的场景:

  • 演示概念:如演示unwrap等特意制造Error。
  • 原型代码:还在原型验证阶段的代码,用于排查可能的错误和完善代码,利用unwrap、expect等。
  • 测试代码:unwrap、expect。
    有些场景可人为确定肯定不会发生错误,可确定Result就是OK,这时可使用unwrap。但是返回结果是Result,编译器还是会认为可能有错,这时使用unwrap。如下边例子IP地址肯定是对的。
use std::net::IpAddr;
fn main() {
    let ip: IpAddr = "192.168.1.1".parse().unwrap();
}

错误处理指导性建议

  • 当代码最可能处于损坏状态时,可用panic!,损坏状态(Bad state):某些假设、保证、约定或不可变性被打破,如非法值、矛盾值、空缺值被传入等。

  • 损坏状态非预期,偶尔发生的情况,或者损坏后无法继续运行,以及没有一个可处理的方法。

posted @ 2023-10-25 17:39  00lab  阅读(119)  评论(0编辑  收藏  举报