Rust 11:错误处理(Option<T>、Result<T,E>、panic、catch_unwind)


错误处理机制的设计是程序架构设计中非常重要的一部分。任何可能的错误,只要没有得到妥善的处理,日后都可能成为系统的隐患。

程序中,错误处理的基本流程为:

  1. 当错误发生时,用适当的方式捕获这个错误;
  2. 对捕获的错误可以立即进行处理,也可以对错误进行传播(propagate),直到不得不处理的地方再处理。
  3. 最终,对内做好错误信息的日志记录,对外给用户展示友好的、易于理解问题所在的错误信息。

几种不同的错误处理方式

使用返回值(错误码)

以C语言为代表,使用函数返回值作为错误记录和传播的手段。
缺点是,开发者需要根据准确的文档,以显示编程的方式处理(或进一步传递)各种返回情况。
然而,不得忽视的一个事实是,代码更新后往往很难保持文档的实时准确更新

使用异常(Exception)

以Java语言为代表。
程序中任何可能出错的地方,都可以抛出异常;而异常可以通过栈回溯(stack unwind)被一层层自动传递,直到遇到捕获异常的地方,如果回溯到main函数还无人捕获,程序就会异常退出。
引入异常来处理错误,其优点是:错误的产生和错误的处理完全被分隔开(关注点分离),调用者不必关心错误,而被调者也不强求调用者关心错误。

不过,使用异常也有缺点:

  1. 在需要打开/关闭资源的情况下,容易出现因为抛出异常而资源没有得到正确关闭和释放的问题。这一点需要编程时特别小心,而且处理起来比较繁琐。
  2. 异常容易被滥用。毕竟,使用异常的代价要远比处理返回值的代价高。

使用类型系统

Rust语言中,就主要使用类型系统来传播和处理错误。主要使用到内置的Option<T>Result<T,E>类型。
同时,当出现比较严重、不可恢复的错误时,Rust支持用panic!宏抛出异常,用来快速退出程序,或者进入外围的catch_unwind代码中。

Option< T >

标准库中的定义:

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

Option<T>是一个枚举类型,要么是Some<T>,要么是None。这能很好地表达有值和无值两种情况,避免出现Java中的NullPointerException

Result< T, E >

#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
    /// Contains the success value
    Ok(T),
    /// Contains the error value
    Err(E),
}

Result<T,E>是也一个枚举类型,要么是正常情况下的结果类型T,要么是错误情况下的结果类型E。
注意一下 #must_use 标注,编译器会对有带有此标注的所有类型做特殊检查:如果该类型对应的值没有在代码中显式地使用,编译器会给出有警告。这能提醒开发者需要将所有错误显示处理。

?操作符

为了让Result<T,E>类型的处理更加方便简洁,Rust中引入了?操作符。
?操作符本质是一个类似如下代码的match匹配。

match result {
  Ok(v) => v,
  Err(e) => return Err(e.into())
}

也就是说:

  • 如果Result是一个E类型的错误值,则提前返回错误,结束当前函数。
  • 如果Result是一个T类型的正确值,则提取出值,方便后续进行链式调用。

代码测试

先来定义一个自己的错误类型:

struct MyError {
    code: i32, 
    msg: String
}
use std::fmt::{Display,Debug,Formatter};
impl std::error::Error for MyError { }
impl Debug for MyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}]{}", self.code, self.msg)
    }
}
impl Display for MyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}]{}", self.code, self.msg)
    }
}

main()函数中测试:

fn main() -> Result<(), MyError> {
    let oneError: Result<(), MyError>= Err(MyError{
        code: -1,
        msg: "error".to_owned(),
    });
    // 问号操作符是针对Result<T,E>类型的一个语法糖
    // 本质是一个match匹配
    // 如果Result是一个E类型的错误值,则提前返回错误,结束当前函数
    // 如果Result是一个T类型的正确值,则提取出值,方便后续进行链式调用
    oneError?;

    Ok(())
}

?操作符的使用限制

  1. ?操作符只能使用在以Option或者Result作为返回值的函数体中
  2. 如果要在main()中使用?操作符。那么首先是要求main()返回值是Option或者Result类型(满足第1条);其次,还要求返回值是要实现std::process::Termination trait的类型。
    rust文档:Termination
pub trait Termination {
    fn report(self) -> i32;
}

其中report()方法返回一个i32值,这与C语言中main()函数要求返回一个整数值一致。
测试代码中的Result<(), MyError>可以作为main函数的返回值类型,是因为标准库中有如下实现:

impl<E: fmt::Debug> Termination for Result<(), E> {
    fn report(self) -> i32 {
        match self {
            Ok(()) => ().report(),
            Err(err) => Err::<!, _>(err).report(),
        }
    }
}

panic!和catch_unwind

当出现比较严重、不可恢复的错误时,Rust支持用panic!宏抛出异常,用来快速退出程序。
如果某些情况下,不希望程序内部的panic!导致程序退出,可以将程序代码包裹在一个闭包中,然后传入std::panic::catch_unwind中执行。

use std::panic;
let result = panic::catch_unwind(||{
    println!("Hello");
});
println!("{}", result.is_ok());//true

let result = panic::catch_unwind(||{
    // 打印异常堆栈信息
    // 被catch_unwind包裹,该panic不会导致进程退出
    panic!("Error in catch_unwind");
});
if(result.is_err()) {
    // 发现异常
    println!("maybe do something");
}

println!("before panic");//before panic
//让程序异常退出
panic!("error exit");
//上一句代码已导致程序异常退出,这一句执行不到
println!("normal exit");
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值